Aide-mémoire SAS - R

SAS
R

Un aide-mémoire pour les statisticiens traduisant des codes standards en SAS et en R, suivant 4 environnements (R-Base, Tidyverse, data.table, Arrow / DuckDb).

Auteur·rice·s
Affiliations

Nassab ABDALLAH

Damien EUZENAT

Sébastien LI-THIAO-TE

Date de publication

19 juin 2024

L’aide-mémoire a pour but de fournir des codes écrits en SAS et d’en donner la traduction en R de différentes manières possibles :

Les codes traduits sont typiques de la production statistique ou la réalisation d’études descriptives.

Ce document s’adresse notamment aux utilisateurs de SAS qui veulent connaître la traduction du code SAS en R, aux utilisateurs de R qui ont besoin de comprendre le code SAS, ainsi qu’aux utilisateurs d’un environnement R qui sont intéressés par la traduction dans un autre environnement R.

Il se veut complémentaire de la documentation en ligne en français Utilit’R, née à l’Insee (https://www.book.utilitr.org/). Le lecteur est invité à s’y référer pour obtenir des informations importantes sur l’utilisation de R et qui ne sont pas discutées dans ce document, comme l’importation de données en R (https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats).

Enfin, si vous souhaitez collaborer à cet aide-mémoire ou nous faire part de votre avis, n’hésitez pas à nous contacter via nos adresses email.

1 Importation des packages

1.1 Installation des packages

Des informations sur l’installation des packages en R sont disponibles sur le site Utilit’R : https://book.utilitr.org/01_R_Insee/Fiche_installer_packages.html.

/* Sans objet pour SAS */
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")
# Les packages doivent au préalable être installés sur le disque dur
# Pour installer un package :
# install.packages("nom_du_package")

1.2 Importation des packages

/* Sans objet pour SAS */
# Sans objet pour R-Base

# Cependant, on importe le package lubridate pour faciliter la gestion des dates
library(lubridate)
# Chargement des packages
# Le tidyverse proprement dit
library(tidyverse)
# Les packages importés par le tidyverse sont :
# - dplyr (manipulation de données)
# - tidyr (réorganisation de bases de données)
# - readr (importation de données)
# - purrr (permet de réaliser des boucles)
# - tibble (format de données tibble, complémentaire du data.frame)
# - stringr (manipulation de chaînes de caractères)
# - ggplot2 (création de graphiques)
# - forcats (gestion des formats "factors")

# Pour manipuler les dates
library(lubridate)
# Pour utiliser le pipe %>%
library(magrittr)

# Documentation de tidyverse
vignette("dplyr")
library(data.table)
# Pour manipuler les dates
library(lubridate)

# Documentation de data.table
?'[.data.table'
#library(duckdb)
#library(arrow)

2 Importation des données

2.1 Mode d’emploi de l’aide-mémoire

Les codes informatiques sont appliqués sur une base de données illustrative fictive. Cette base est importée à cette étape. Aussi, pour répliquer les codes sur sa machine, le lecteur doit d’abord exécuter le code d’importation de la base de données ci-dessous.

Les codes sont majoritairement exécutables indépendamment les uns des autres. Les codes de la partie “Les jointures de bases” nécessitent cependant l’importation des bases réalisée lors de la première section de la partie.

2.2 Création d’une base de données d’exemple

/* Données fictives sur des formations */
data donnees_sas;
  infile cards dsd dlm='|';
  format Identifiant $3. Sexe 1. CSP $1. Niveau $30. Date_naissance ddmmyy10. Date_entree ddmmyy10. Duree Note_Contenu Note_Formateur Note_Moyens
         Note_Accompagnement Note_Materiel poids_sondage 4.1 CSPF $25. Sexef $5.;
  input Identifiant $ Sexe CSP $ Niveau $ Date_naissance :ddmmyy10. Date_entree :ddmmyy10. Duree Note_Contenu Note_Formateur Note_Moyens
        Note_Accompagnement Note_Materiel poids_sondage CSPF $ Sexef $;
  cards;
  173|2|1|Qualifié|17/06/1998|01/01/2021|308|12|6|17|4|19|117.1|Cadre|Femme
  173|2|1|Qualifié|17/06/1998|01/01/2022|365|6||12|7|14|98.3|Cadre|Femme
  173|2|1|Qualifié|17/06/1998|06/01/2022|185|8|10|11|1|9|214.6|Cadre|Femme
  173|2|1|Non qualifié|17/06/1998|02/01/2023|365|14|15|15|10|8|84.7|Cadre|Femme
  174|1|1|Qualifié|08/12/1984|17/08/2021|183|17|18|20|15|12|65.9|Cadre|Homme
  175|1|1|Qualifié|16/09/1989|21/12/2022|730|5|5|8|4|9|148.2|Cadre|Homme
  198|2|3|Non qualifié|17/03/1987|28/07/2022|30|10|10|10|16|8|89.6|Employé|Femme
  198|2|3|Qualifié|17/03/1987|17/11/2022|164|11|7|6|14|13|100.3|Employé|Femme
  198|2|3|Qualifié|17/03/1987|21/02/2023|365|9|20|3|4|17|49.3|Employé|Femme
  168|1|2|Qualifié|30/07/2002|04/09/2019|365|18|11|20|13|15|148.2|Profession intermédiaire|Homme
  211|2|3|Non qualifié||17/12/2021|135|16|16|15|12|9|86.4|Employé|Femme
  278|1|5|Qualifié|10/08/1948|07/06/2018|365|14|10|6|8|12|99.2|Retraité|Homme
  347|2|5|Qualifié|13/09/1955||180|12|5|7|11|12|105.6|Retraité|Femme
  112|1|3|Non qualifié|13/09/2001|02/03/2022|212|3|10|11|9|8|123.1|Employé|Homme
  112|1|3|Non qualifié|13/09/2001|01/03/2021|365|7|13|8|19|2|137.4|Employé|Homme
  112|1|3|Qualifié|13/09/2001|01/12/2023|365|9|||||187.6|Employé|Homme
  087|2|4|Non qualifié|||365||10||||87.3|Ouvrier|Femme
  087|2|4|Non qualifié||31/10/2020|365|||11|||87.3|Ouvrier|Femme
  099|1|4|Qualifié|06/06/1998|01/03/2021|364|12|11|10|12|13|169.3|Ouvrier|Homme
  099|1|4|Qualifié|06/06/1998|01/03/2022|364|12|11|10|12|13|169.3|Ouvrier|Homme
  099|1|4|Qualifié|06/06/1998|01/03/2023|364|12|11|10|12|13|169.3|Ouvrier|Homme
  187|2|2|Qualifié|05/12/1986|01/01/2022|364|10|10|10|10|10|169.3|Profession intermédiaire|Femme
  187|2|2|Qualifié|05/12/1986|01/01/2023|364|10|10|10|10|10|234.1|Profession intermédiaire|Femme
  689|1|1||01/12/2000|06/11/2017|123|9|7|8|13|16|189.3|Cadre|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  765|1|4|Non qualifié|26/12/1995|17/04/2020|160|13|10|12|18|10|45.9|Ouvrier|Homme
  ;
run;

/* Date de sortie du dispositif : ajout de la durée à la date d'entrée */
data donnees_sas;
  set donnees_sas;
  format date_sortie ddmmyy10.;
  date_sortie = intnx('day', date_entree, duree);
run;
# Données fictives sur des formations
donnees_rbase <- data.frame(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
)

# Mise en forme des données

# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
# Renommer les colonnes de la base
colnames(donnees_rbase) <- tolower(colnames(donnees_rbase))
# Autre possibilité
setNames(donnees_rbase, tolower(names(donnees_rbase)))

# On a importé toutes les variables en format caractère
# On convertit certaines variables en format numérique
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_rbase[, enNumerique] <- lapply(donnees_rbase[, enNumerique], as.integer)
donnees_rbase$poids_sondage <- as.numeric(donnees_rbase$poids_sondage)

# On récupère les variables dont le nom débute par le mot "date"
enDate <- names(donnees_rbase)[grepl("date", tolower(names(donnees_rbase)))]
# On remplace / par - dans les dates
donnees_rbase[, enDate] <- lapply(donnees_rbase[, enDate], function(x) gsub("/", "-", x))
# On exprime les dates en format Date
donnees_rbase[, enDate] <- lapply(donnees_rbase[, enDate], lubridate::dmy)

# Date de sortie du dispositif
donnees_rbase$date_sortie <- donnees_rbase$date_entree + lubridate::days(donnees_rbase$duree)
# Données fictives sur des formations
donnees_tidyverse <- tibble(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
  
)

# Mise en forme des données

# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
# Renommer les colonnes de la base en minuscule
donnees_tidyverse <- donnees_tidyverse %>% rename_with(tolower)
# Autre solution
donnees_tidyverse <- donnees_tidyverse %>% 
  magrittr::set_colnames(value = casefold(colnames(.), upper = FALSE))

# On a importé toutes les variables en format caractère
# On convertit certaines variables en format numérique
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# On convertit certaines variables au format date
# On récupère d'abord les variables dont le nom débute par le mot "date"
enDate <- names(donnees_tidyverse)[grepl("date", tolower(names(donnees_tidyverse)))]

donnees_tidyverse <- donnees_tidyverse %>%  
  mutate_at(enNumerique, as.integer) %>% 
  mutate(poids_sondage = as.numeric(poids_sondage)) %>% 
  mutate_at(enDate, lubridate::dmy)

# Date de sortie du dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_sortie = date_entree + lubridate::days(duree))
# Données fictives sur des formations
donnees_datatable <- data.table(
  Identifiant = c("173", "173", "173", "173", "174", "175", "198", "198", "198", "168", "211", "278", "347", "112", "112", "112", "087", "087", "099", "099", "099", "187", "187", "689", "765", "765", "765"),
  Sexe = c("2", "2", "2", "2", "1", "1", "2", "2", "2", "1", "2", "1", "2", "1", "1", "1", "2", "2", "1", "1", "1", "2", "2", "1", "1", "1", "1"),
  CSP = c("1", "1", "1", "1", "1", "1", "3", "3", "3", "2", "3", "5", "5", "3", "3", "3", "4", "4", "4", "4", "4", "2", "2", "1", "4", "4", "4"),
  Niveau = c("Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Non qualifié", "Qualifié", "Qualifié", "Non qualifié", 
             "Non qualifié", "Qualifié", "Non qualifié", "Non qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", "Qualifié", NA, "Non qualifié", "Non qualifié", "Non qualifié"),
  Date_naissance = c("17/06/1998", "17/06/1998", "17/06/1998", "17/06/1998", "08/12/1984", "16/09/1989", "17/03/1987", "17/03/1987", "17/03/1987", "30/07/2002", NA, "10/08/1948", 
                     "13/09/1955", "13/09/2001", "13/09/2001", "13/09/2001", NA, NA, "06/06/1998", "06/06/1998", "06/06/1998", "05/12/1986", "05/12/1986", "01/12/2000", "26/12/1995", "26/12/1995", "26/12/1995"),
  Date_entree = c("01/01/2021", "01/01/2022", "06/01/2022", "02/01/2023", "17/08/2021", "21/12/2022", "28/07/2022", "17/11/2022", "21/02/2023", "04/09/2019", "17/12/2021", "07/06/2018", NA, "02/03/2022", "01/03/2021", "01/12/2023", NA, 
                  "31/10/2020", "01/03/2021", "01/03/2022", "01/03/2023", "01/01/2022", "01/01/2023", "06/11/2017", "17/04/2020", "17/04/2020", "17/04/2020"),
  Duree = c("308", "365", "185", "365", "183", "730", "30", "164", "365", "365", "135", "365", "180", "212", "365", "365", "365", "365", "364", "364", "364", "364", "364", "123", "160", "160", "160"),
  Note_Contenu = c("12", "6", "8", "14", "17", "5", "10", "11", "9", "18", "16", "14", "12", "3", "7", "9", NA, NA, "12", "12", "12", "10", "10", "9", "13", "13", "13"),
  Note_Formateur = c("6", NA, "10", "15", "18", "5", "10", "7", "20", "11", "16", "10", "5", "10", "13", NA, "10", NA, "11", "11", "11", "10", "10", "7", "10", "10", "10"),
  Note_Moyens = c("17", "12", "11", "15", "20", "8", "10", "6", "3", "20", "15", "6", "7", "11", "8", NA, NA, "11", "10", "10", "10", "10", "10", "8", "12", "12", "12"),
  Note_Accompagnement = c("4", "7", "1", "10", "15", "4", "16", "14", "4", "13", "12", "8", "11", "9", "19", NA, NA, NA, "12", "12", "12", "10", "10", "13", "18", "18", "18"),
  Note_Materiel = c("19", "14", "9", "8", "12", "9", "8", "13", "17", "15", "9", "12", "12", "8", "2", NA, NA, NA, "13", "13", "13", "10", "10", "16", "10", "10", "10"),
  poids_sondage = c("117.1", "98.3", "214.6", "84.7", "65.9", "148.2", "89.6", "100.3", "49.3", "148.2", "86.4", "99.2", "105.6", "123.1", "137.4", "187.6", "87.3", "87.3",
                    "169.3", "169.3", "169.3", "169.3", "234.1", "189.3", "45.9", "45.9", "45.9"),
  CSPF = c("Cadre", "Cadre", "Cadre", "Cadre", "Cadre","Cadre", "Employé", "Employé", "Employé", "Profession intermédiaire", "Employé", "Retraité", "Retraité", "Employé",
           "Employé", "Employé", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Ouvrier", "Profession intermédiaire", "Profession intermédiaire", "Cadre", "Ouvrier", "Ouvrier",
           "Ouvrier"),
  Sexef = c("Femme", "Femme", "Femme", "Femme", "Homme", "Homme", "Femme", "Femme", "Femme", "Homme", "Femme", "Homme", "Femme", "Homme", "Homme", "Homme", "Femme", "Femme",
            "Homme", "Homme", "Homme", "Femme", "Femme", "Homme", "Homme", "Homme", "Homme")
)

# Mise en forme des données

# Extraire les noms des variables de la base
# R est sensible à la casse, il est pertinent d'harmoniser les noms des variables en minuscule
nomCol <- tolower(colnames(donnees_datatable))
# Renommer les colonnes de la base
colnames(donnees_datatable) <- tolower(colnames(donnees_datatable))

# On convertit certaines variables en format 'numeric'
enNumerique <- c("duree", "note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Ne pas oublier le . devant SDcols !!!!
donnees_datatable[, lapply(.SD, as.integer), .SDcols = enNumerique]
# Autre solution
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
for (j in enNumerique) {
  set(donnees_datatable, j = j, value = as.numeric(donnees_datatable[[j]]))
}
donnees_datatable[, poids_sondage := as.numeric(poids_sondage)]

# On récupère les variables dont le nom débute par le mot "date"
varDates <- names(donnees_datatable)[grepl("date", tolower(names(donnees_datatable)))]
# On remplace / par - dans les dates
donnees_datatable[, (varDates) := lapply(.SD, function(x) gsub("/", "-", x)), .SDcols = varDates]
# On exprime les dates en format Date
donnees_datatable[, (varDates) := lapply(.SD, lubridate::dmy), .SDcols = varDates]

# Date de sortie du dispositif
donnees_datatable[, date_sortie := date_entree + lubridate::days(duree)]

Duckdb est un serveur SQL séparé de la session R. Les calculs sont effectués en dehors de R et l’espace mémoire est distinct de celui de R. Au lieu d’accéder directement aux données, il faut passer par un objet connection qui contient l’adresse du serveur, un peu comme lorsque l’on se connecte à un serveur web. Ici en particulier, il est nécessaire de transférer les données vers duckdb.

# Ouvrir une connexion au serveur duckdb
con <- DBI::dbConnect(duckdb::duckdb()); 

# On "copie" les données dans une table du nom table_duckdb
con %>% duckdb::duckdb_register(name = "table_duckdb", df = donnees_tidyverse)

con %>% tbl("table_duckdb")

# Fermer la connexion au serveur duckdb
DBI::dbDisconnect(con, shutdown = TRUE)

Pour la suite, on suppose que la connexion est ouverte sous le nom con, et que les données sont accessibles par la requête requete_duckdb. Le code modifiera la requête, mais pas la table dans le serveur SQL.

con <- DBI::dbConnect(duckdb::duckdb()); 
con %>% duckdb::duckdb_register(name = "table_duckdb", df = donnees_tidyverse)
requete_duckdb <- con %>% tbl("table_duckdb")

N.B. Duckdb est envisagé pour des traitements sans charger des données en mémoire, par exemple en lisant directement un fichier .parquet sur le disque dur. Dans ce cas, les opérations sont effectuées à la volée, mais n’affectent pas les fichiers source.

2.3 Manipulation du format de la base de données

/* Sans objet pour SAS */
# On vérifie que la base importée est bien un data.frame
is.data.frame(donnees_rbase)

# Format de la base
class(donnees_rbase)
# On vérifie que la base importée est bien un tibble
is_tibble(donnees_tidyverse)

# Transformation en tibble, le format de Tidyverse
donnees_tidyverse <- tibble::as_tibble(donnees_tidyverse)

# Format de la base
class(donnees_tidyverse)
# On vérifie que la base est bien un data.table
is.data.table(donnees_datatable)

# Transformation en data.frame
setDF(donnees_datatable)
is.data.frame(donnees_datatable)

# Transformation en data.table
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setDT(donnees_datatable)
is.data.table(donnees_datatable)
# Autre possibilité
donnees_datatable <- as.data.table(donnees_datatable)

# Est-ce une liste ?
is.list(donnees_datatable)

# Format de la base
class(donnees_datatable)

2.4 Importation de données extérieures

Importer des données extérieures dans SAS ou R est sans doute la première tâche à laquelle est confronté l’utilisateur de ces logiciels. Ce point important est décrit sur le site Utilit’R : https://www.book.utilitr.org/03_fiches_thematiques/fiche_import_fichiers_plats.

Pour importer des fichiers :

Quelques éléments additionnels non couverts dans Utilit’R sont présentés ici.

2.4.1 La fonction ReadLines() en R

La fonction readLines() de R peut s’avérer utile lors de l’import de fichiers très volumineux. Elle permet de n’importer que les premières lignes du fichier, et ainsi de visualiser rapidement le contenu des données et la nature de l’éventuel séparateur de colonnes.

Les options de la fonction utiles sont :

  • con : chemin du fichier à importer
  • n : nombre maximal de lignes du fichier lues
  • encoding : définir l’encodage du fichier (“UTF-8” ou “Latin-1”)

2.4.2 Spécificité des environnements

/* À écrire */

On utilisera les fonctions read.table, read.csv et read.csv2.

On utilisera la fonction fread : https://book.utilitr.org/03_Fiches_thematiques/Fiche_import_fichiers_plats.html#importer-un-fichier-avec-le-package-data.table.

Une option utile non présentée dans le lien est : keepLeadingZeros. Si cette option est valorisée à TRUE, les valeurs numériques précédées par des 0 seront importées sous forme caractère et le zéro initial sera conservé.

3 Préambule

3.1 Chemin du bureau de l’utilisateur

/* On vide la log */
dm "log; clear; ";
/* On récupère déjà l'identifiant de l'utilisateur (systèmes Windows) */
%let user = &sysuserid;
/* Chemin proprement dit */
%let bureau = C:\Users\&user.\Desktop;
libname bur "&bureau.";
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")
# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")
# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")
# On récupère déjà l'identifiant de l'utilisateur
user <- Sys.getenv("USERNAME")
# Chemin proprement dit
chemin <- file.path("C:/Users", user, "Desktop")

3.2 Affichage de l’année

/* Année courante */
%let an = %sysfunc(year(%sysfunc(today())));
/* & (esperluette) indique à SAS qu'il doit remplacer an par sa valeur définie par le %let */
%put Année : &an.;
/* Autre possibilité */
data _null_;call symput('annee', strip(year(today())));run;
%put Année (autre méthode) : &annee.;
/* Année passée */
%put Année passée : %eval(&an. - 1);
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))
# Année passée
annee_1 <- annee - 1
paste0("Année passée: ", annee_1)
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))
# Année passée
annee_1 <- annee - 1
paste0("Année passée: ", annee_1)
# Année courante
annee <- lubridate::year(Sys.Date())
sprintf("Année : %04d", annee)
print(paste0("Année : ", annee))
# Année passée
annee_1 <- annee - 1
paste0("Année passée: ", annee_1)

3.3 Construction des instructions if / else

%macro Annee(an);
  %if &an. >= 2024 %then %put Nous sommes en 2024 ou après !;
  %else %put Nous sommes avant 2024 !;
%mend Annee;
%Annee(&an.);
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après")
#}
#else {
#  print("Nous sommes avant 2024")
#}
## Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après")
} else {
  print("Nous sommes avant 2024 !")
}
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après")
#}
#else {
#  print("Nous sommes avant 2024")
#}
## Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après")
} else {
  print("Nous sommes avant 2024 !")
}
# Construction incorrecte ! Le else doit être sur la même ligne que le {
#if (annee >= 2024) {
#  print("Nous sommes en 2024 ou après")
#}
#else {
#  print("Nous sommes avant 2024")
#}
## Construction correcte ! Le else doit être sur la même ligne que le {
if (annee >= 2024) {
  print("Nous sommes en 2024 ou après")
} else {
  print("Nous sommes avant 2024 !")
}

3.4 Répertoire de travail

/* Afficher le répertoire de travail par défaut (la Work) */
%let chemin_work = %sysfunc(pathname(work));
%put &chemin_work.;
proc sql;
  select path from dictionary.libnames where libname = "WORK";
quit;
/* Définir le répertoire de travail, si besoin */
/* libname "nom du répertoire";
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir="nom du répertoire")
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir="nom du répertoire")
# Afficher le répertoire de travail
getwd()

# Définir le répertoire de travail, si besoin
#setwd(dir="nom du répertoire")

3.5 Autres points à connaître

/* Mise en garde : certains codes SAS pourraient aussi avec profit être écrits en langage SAS IML (Interactive Matrix Language).
   Cet aide-mémoire n'ayant pas vocation à être un dictionnaire SAS, cette méthode d'écriture n'est pas proposée ici. */
# Le pipe peut aussi être utilisé avec R-Base et data.table
# Le pipe permet d'enchaîner des opérations sur une même base
1:10 |> sum()

# R-Base est réputé plus lent que ses concurrents, ce qui est souvent vrai.
# Mais certaines fonctions en R-Base être très rapides (rowsum, rowSums, tapply, etc.)
# tidyverse promeut l'utilisation du pipe (%>%), qui permet d'enchaîner des opérations sur une même base modifiée successivement.
# 2 types de pipes existent, le pipe de magrittr (%>%) et le pipe de R-Base (|>, à partir de la version 4.1)
# Les fonctionnalités simples des deux opérateurs sont identiques, mais il existe des différences.
# Dans cet aide-mémoire, le pipe de magrittr (%>%) est privilégié.

# Le tidyverse peut s'utiliser sans pipe, mais le pipe simplifie la gestion des programmes.
# Les autres environnements (R-Base, data.table) peuvent aussi se présenter avec le pipe.
# Principe de base de data.table
#dt[i, j, by]
# i : sélection de lignes (instructions )
# j : sélection et manipulation de colonnes
# by : groupements

4 Informations sur la base de données

4.1 Avoir une vue d’ensemble des données

/* Statistiques globales sur les variables numériques */
proc means data = donnees_sas n mean median min p10 p25 median p75 p90 max;var _numeric_;run;
/* Statistiques globales sur les variables caractères */
proc freq data = donnees_sas;tables _character_ / missing;run;
# Informations sur les variables
str(donnees_rbase)
# Statistiques descriptives des variables de la base
summary(donnees_rbase)
library(Hmisc)
Hmisc::describe(donnees_rbase)
# Visualiser la base de données
View(donnees_rbase)
# Informations sur les variables
donnees_tidyverse %>% str()
donnees_tidyverse %>% glimpse()
# Statistiques descriptives des variables de la base
donnees_tidyverse %>% summary()
# Visualiser la base de données
donnees_tidyverse %>% View()
# Informations sur les variables
str(donnees_datatable)
# Statistiques descriptives des variables de la base
summary(donnees_datatable)
# Visualiser la base de données
View(donnees_datatable)

On accède aux données du serveur SQL DuckDB au travers de l’objet requete_duckdb, qui est une requête (avec l’adresse du serveur) et non pas un dataframe ou un tibble. Comme l’accès n’est pas direct, la plupart des fonctions du tidyverse fonctionnent, mais opèrent sur “l’adresse du serveur DuckDB” au lieu d’opérer sur les valeurs (nombres, chaînes de caractères). A part glimpse, la plupart des fonctions ne renvoient pas un résultat exploitable.

# Informations sur les variables
# requete_duckdb %>% str() 
requete_duckdb %>% glimpse() # préférer glimpse()
# requete_duckdb %>% summary()
# requete_duckdb %>% View() 

4.2 Extraire les x premières lignes de la base (10 par défaut)

%let x = 10;
proc print data = donnees_sas (firstobs = 1 obs = &x.);run;
/* Ou alors */
data Lignes&x.;set donnees_sas (firstobs = 1 obs = &x.);proc print;run;
x <- 10
donnees_rbase[1:x, ]
head(donnees_rbase, x)
x <- 10
donnees_tidyverse %>% 
  slice(1:x)
donnees_datatable[, first(.SD, 10)]
donnees_datatable[, .SD[1:10]]
first(donnees_datatable, 10)
head(donnees_datatable, 10)

DuckDB affiche les dix premières lignes par défaut lorsque l’on évalue une requête, comme indiqué dans le code ci-dessous.

requete_duckdb
# Ceci est équivalent au code suivant
# requete_duckdb %>% print(n=10)

Attention, comme il n’y a pas d’ordre en SQL, il faut ordonner les lignes si on veut un résultat reproductible. C’est une opération qui peut être couteuse en temps CPU.

requete_duckdb %>% arrange(duree) %>% print()

L’objet requete_duckdb est bien une requête (i.e. une liste à deux éléments) même si on peut en afficher le résultat avec la fonction print. Notamment, les informations restent dans la mémoire de DuckDB. Il faut demander explicitement le transfert du résultat vers la session R avec la fonction collect(). On obtient alors un objet de type data.frame ou au lieu de tbl_duckdb_connection.

class(requete_duckdb)
resultat_tibble <- requete_duckdb %>% collect()
class(resultat_tibble)

La fonction collect() transfère l’ensemble des données. Pour obtenir uniquement 10 lignes, il faut utiliser l’une des fonctions slice_* (cf documentation). On conseille slice_min ou slice_max qui indiquent explicitement l’ordre utilisé.

requete_duckdb %>% slice_max(duree, n=4, with_ties=FALSE) # with_ties = TRUE retourne les cas d'égalité, donc plus de 4 lignes

En DuckDB et/ou sur un serveur SQL, on déconseille les fonctions head (qui ne respecte pas toujours l’ordre indiqué par arrange) ou top_n (superseded). La fonction slice en fonctionne pas : elle ne peut pas respecter l’ordre.

4.3 Extraire les x dernières lignes de la base (10 par défaut)

%let x = 10;
proc sql noprint;select count(*) into :total_lignes from donnees_sas;quit;
%let deb = %eval(&total_lignes. - &x. + 1);
data Lignes_&x.;set donnees_sas (firstobs = &deb. obs = &total_lignes.);run;
x <- 10
tail(donnees_rbase, x)

Alternativement

donnees_rbase[ ( nrow(donnees_rbase) - x ) : nrow(donnees_rbase), ]

Les parenthèses sont importantes. Comparer les deux expressions ! Bon exemple du recycling

( nrow(donnees_rbase) - x ) : nrow(donnees_rbase)
nrow(donnees_rbase) - x : nrow(donnees_rbase)
donnees_tidyverse %>% 
  slice( (n()-10) : n())
donnees_datatable[, last(.SD, 10)]
last(donnees_datatable, 10)
tail(donnees_datatable, 10)

Alternativement

Mêmes remarques que pour les premières lignes : il n’y a pas d’ordre a priori en SQL. On conseille slice_min ou slice_max qui indiquent explicitement l’ordre utilisé, et l’on déconseille slice et tail.

requete_duckdb %>% slice_min(duree, n=5, with_ties=FALSE) # with_ties = TRUE retourne les cas d'égalité, donc plus de 5 lignes

4.4 Nombre de lignes et de colonnes dans la base

/* Nombre de lignes */
proc sql;select count(*) as Nb_Lignes from donnees_sas;quit;
proc sql;
  select count(*) as Nb_Lignes, count(distinct identifiant) as Nb_Identifiants
  from donnees_sas;
quit;
/* Liste des variables de la base dans la base Var */
proc contents data = donnees_sas out = Var noprint;run;
/* Nombre de colonnes */
proc sql;select count(*) as Nb_Colonnes from Var;run;
# Les syntaxes dim(donnees_rbase)[1] et dim(donnees_rbase)[2] sont plus rapides que nrow() et ncol()
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", dim(donnees_rbase)[1], dim(donnees_rbase)[2])
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", nrow(donnees_rbase), ncol(donnees_rbase))
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d",
        donnees_tidyverse %>% nrow(),
        donnees_tidyverse %>% ncol())
# Nombre de lignes
donnees_tidyverse %>% nrow()
# Nombre de colonnes
donnees_tidyverse %>% ncol()
# Les syntaxes dim(donnees_rbase)[1] et dim(donnees_rbase)[2] sont plus rapides que nrow() et ncol()
dim(donnees_datatable) ; dim(donnees_datatable)[1] ; dim(donnees_datatable)[2]
dim(donnees_datatable) ; nrow(donnees_datatable) ; ncol(donnees_datatable)
sprintf("Nombre de lignes : %d | Nombre de colonnes : %d", dim(donnees_datatable)[1], dim(donnees_datatable)[2])

# Autre solution
donnees_datatable[, .N]

Duckdb/SQL ne connaît pas le nombre de lignes sans un calcul. Il faut faire count().

#Nombre de lignes
requete_duckdb %>% nrow() # retourne NA
requete_duckdb %>% count() # correct

#Nombre de colonnes
requete_duckdb %>%  ncol()

4.5 Liste des variables de la base

/* Par ordre d'apparition dans la base */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;select name into :nom_col separated by " " from Var order by varnum;run;

/* On affiche les noms des variables */
%put Liste des variables : &nom_col.;

/* Par ordre alphabétique */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;select name into :nom_col separated by " " from Var;run;

/* On affiche les noms des variables */
%put Liste des variables : &nom_col.;
# Liste des variables par ordre d'apparition dans la base
names(donnees_rbase)
colnames(donnees_rbase)
# Liste des variables par ordre alphabétique
ls(donnees_rbase)
sort(colnames(donnees_rbase))
# Liste des variables par ordre d'apparition dans la base
donnees_tidyverse %>% colnames()
# Liste des variables par ordre alphabétique
donnees_tidyverse %>% colnames() %>% sort()
# Liste des variables par ordre d'apparition dans la base
colnames(donnees_datatable)
# Liste des variables par ordre alphabétique
sort(colnames(donnees_datatable))
requete_duckdb %>% colnames()

4.6 Nombre d’identifiants uniques et nombre de lignes dans la base

proc sql;
  select count(*) as Nb_Lignes, count(distinct identifiant) as Nb_Identifiants_Uniques
  from donnees_sas;
quit;
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        nrow(donnees_rbase),
        length(unique(donnees_rbase$identifiant)))
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        donnees_tidyverse %>% nrow(),
        donnees_tidyverse %>% select(identifiant) %>%
          n_distinct()
        )
# Autre solution pour le nombre d'identifiants uniques
donnees_tidyverse %>% select(identifiant) %>% n_distinct()
donnees_tidyverse %>% distinct(identifiant) %>% nrow()
sprintf("La base de données contient %d lignes et %d identifiants uniques !",
        nrow(donnees_datatable),
        donnees_datatable[, uniqueN(identifiant)])
# Identifiants uniques
donnees_datatable[, uniqueN(identifiant)]
requete_duckdb %>% nrow()
requete_duckdb %>% distinct(identifiant) %>% count()

Note : on a vu que nrow ne fonctionne pas en DuckDB.

4.7 Quelle est la position de la variable date_entree ?

%let var = date_entree;
proc contents data = donnees_sas out = Var noprint;run;
proc sql;
  select varnum as Position from Var where lowcase(NAME) = "&var.";
run;
variable <- "date_entree"
pos <- match(variable, names(donnees_rbase))
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, donnees_tidyverse %>% colnames())
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, names(donnees_datatable))
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)
variable <- "date_entree"
pos <- match(variable, requete_duckdb %>% colnames())
sprintf("La variable %s se trouve en colonne n°%s !", variable, pos)

4.8 Variables qui débutent par “note”

proc contents data = donnees_sas out = Variables;run;
proc sql;select Name from Variables where upcase(substr(Name,1,4)) = "NOTE";run;
grep("^note", names(donnees_rbase), ignore.case = TRUE, value = TRUE)
names(donnees_tidyverse) |> str_subset("^note")
grep("^note", names(donnees_datatable), ignore.case = TRUE, value = TRUE)

5 Type des variables (int, char, bool, etc.)

5.1 Afficher le type des variables

proc contents data = donnees_sas;run;

/* On supprime la base Var temporaire */
proc datasets lib = Work nolist;delete Var;run;
sapply(donnees_rbase, class)
purrr::map(donnees_tidyverse, class)
class(donnees_tidyverse)
donnees_datatable[, lapply(.SD, class)]

On ne peut pas appliquer directement la fonction class sur un objet de type connection. Cependant, DuckDB affiche le type des variables dans un print. On peut également appliquer la fonction class sur un extrait des données (après collect).

purrr::map(requete_duckdb %>% select(c(1,2)) %>% head() %>% collect(), class)
class(requete_duckdb)

5.2 Convertir le type d’une seule variable

data donnees_sas;
  set donnees_sas;
  /* Transformer la variable Sexe en caractère */
  Sexe_car = put(Sexe, $1.);
  /* Transformer la variable Sexe_car en numérique */
  Sexe_num = input(Sexe_car, 1.);
  /* Transformer une date d'un format caractère à un format Date */
  format date $10.;
  date = "01/01/2000";
  format date_sas yymmdd10.;
  date_sas = input(date, ddmmyy10.);
run;
# Transformer la variable sexe en numérique
donnees_rbase$sexe_numerique <- as.numeric(donnees_rbase$sexe)
# Transformer la variable sexe_numerique en caractère
donnees_rbase$sexe_caractere <- as.character(donnees_rbase$sexe_numerique)
# Transformer une date d'un format caractère à un format Date
donnees_rbase$date_r <- lubridate::dmy("01/01/2000")
# Transformer la variable sexe en numérique
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(sexe_numerique = as.numeric(sexe))

# Transformer la variable sexe_numerique en caractère
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(sexe_caractere = as.character(sexe_numerique))

# Transformer une date d'un format caractère à un format Date
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(date_r = lubridate::dmy("01/01/2000"))
# Transformer la variable sexe en numérique
donnees_datatable[, sexe_numerique := as.numeric(sexe)]
# Transformer la variable sexe_numerique en caractère
donnees_datatable[, sexe_caractere := as.numeric(sexe_numerique)]
# Transformer une date d'un format caractère à un format Date
donnees_datatable[, date_r := lubridate::dmy("01/01/2000")]
requete_duckdb %>%  
  mutate(sexe_numerique = as.numeric(sexe)) %>% # Transformer la variable sexe en numérique
  mutate(sexe_caractere = as.character(sexe_numerique)) %>% # Transformer la variable sexe_numerique en caractère
  select(starts_with("sexe")) %>% print(n=5)

En DuckDB, plusieurs syntaxes sont possibles pour transformer une chaîne de caractères en date. Sauf lorsque la chaîne de caractères n’est pas au format YYYY-MM-DD. On conseille de passer par la fonction strptime de DuckDB pour indiquer le format de la date.

# Transformer une date d'un format caractère à un format Date
requete_duckdb %>%  
  mutate(date_0 = as.Date("2000-01-01")) %>% 
  mutate(date_1 = as.Date(strptime("01/01/2000","%d/%m/%Y"))) %>% 
  # mutate(date_r = lubridate::dmy("01/01/2000")) %>% # no known SQL translation
  select(starts_with("date")) %>% print(n=5)

5.3 Convertir le type de plusieurs variables

enNumerique <- c("duree", "note_contenu", "note_formateur")
enDate <- c('date_naissance', 'date_entree')

requete_duckdb %>%  
  mutate_at(enNumerique, as.integer) %>% 
  mutate_at(enDate, as.character) %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%Y-%m-%d'))) %>% # strptime est une fonction duckdb
  select(enNumerique, enDate) %>% print(n=5)

6 Sélectionner des lignes et des colonnes

6.1 Selection de colonnes par position

%let pos = 1;
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " "
  from Var
  where varnum = &pos.;
run;
data Colonnes;set donnees_sas (keep = &nom_col.);run;
proc datasets lib = Work nolist;delete Var;run;
pos <- 1
# Résultat sous forme de vecteur caractère
id <- donnees_rbase[[pos]] ; class(id)
id <- donnees_rbase[, pos] ; class(id)
# Résultat sous forme de data.frame
id <- donnees_rbase[pos] ; class(id)
id <- donnees_rbase[, pos, drop = FALSE] ; class(id)
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(1)
class(id)
pos <- 1
id <- donnees_tidyverse %>% pull(all_of(pos))
class(id)
# Sous forme de tibble
id <- donnees_tidyverse %>% select(1)
class(id)
pos <- 1
id <- donnees_tidyverse %>% select(all_of(pos))
class(id)
pos <- 1
# Résultat sous forme de vecteur caractère
id <- donnees_datatable[[pos]] ; class(id)
# Résultat sous forme de data.table
id <- donnees_datatable[pos] ; class(id)

En DuckDB, il y a une vraie différence entre select et pull. Dans le premier cas, les calculs restent du côté DuckDB, et c’est donc le moteur SQL qui continue à exécuter les calculs. Avec pull, le résultat est un tibble et les données sont transférées à la session R.

requete_duckdb %>% select(3)

6.2 Selection de colonnes par nom

data Colonnes;set donnees_sas (keep = identifiant);run;
data Colonnes;set donnees_sas;keep identifiant;run;
# Résultat sous forme de vecteur caractère
id <- donnees_rbase$identifiant ; class(id)
id <- donnees_rbase[["identifiant"]] ; class(id)
id <- donnees_rbase[, "identifiant"] ; class(id)
# Résultat sous forme de data.frame
class(donnees_rbase[, "identifiant"])
# Attention, utilisation du drop = FALSE étrange
# En fait, l'affectation par [] a pour option par défaut drop = TRUE. Ce qui implique que si l'affectation renvoie
# un data.frame d'1 seule colonne, l'objet sera transformé en objet plus simple (vecteur en l'occurrence)
class(donnees_rbase[, "identifiant", drop = FALSE])
id <- donnees_rbase["identifiant"] ; class(id)
id <- donnees_rbase[, "identifiant", drop = FALSE] ; class(id)
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(identifiant)
id <- donnees_tidyverse %>% pull("identifiant")
# Sous forme de tibble
id <- donnees_tidyverse %>% select(identifiant)
id <- donnees_tidyverse %>% select("identifiant")
# Résultat sous forme de vecteur caractère
id <- donnees_datatable$identifiant ; class(id)
id <- donnees_datatable[["identifiant"]] ; class(id)
id <- donnees_datatable[, identifiant] ; class(id)
# Résultat sous forme de data.table
id <- donnees_datatable[, "identifiant"] ; class(id)
id <- donnees_datatable[, .("identifiant")] ; class(id)
id <- donnees_datatable[, list("identifiant")] ; class(id)
requete_duckdb %>% select(identifiant)
requete_duckdb %>% select("identifiant")
requete_duckdb %>% select(any_of("identifiant"))

Note : on déconseille l’utilisation de select sur des chaînes de caractère : certaines fonction du tidyvers nécessient de passer par les opérateurs any_of ou all_of pour ce genre d’opérations (distinct par exemple).

6.3 Selection de colonnes par un vecteur contenant des chaînes de caractères

%let var = identifiant Sexe note_contenu;
data Colonnes;
  /* Sélection de colonnes */
  set donnees_sas (keep = &var.);
  /* Selection de colonnes */
  keep &var.;
run;
variable <- "identifiant"
# Résultat sous forme de vecteur caractère
id <- donnees_rbase[, variable] ; class(id)
id <- donnees_rbase[[variable]] ; class(id)
# Résultat sous forme de data.frame
id <- donnees_rbase[variable] ; class(id)
id <- donnees_rbase[, variable, drop = FALSE] ; class(id)
variable <- "identifiant"
# Sous forme de vecteur
id <- donnees_tidyverse %>% pull(all_of(variable))
# Sous forme de tibble
id <- donnees_tidyverse %>% select(all_of(variable))
# Résultat sous forme de vecteur caractère
variable <- "identifiant"
id <- donnees_datatable[[variable]] ; class(id)
# Résultat sous forme de data.table
id <- donnees_datatable[, .(variable)] ; class(id)
id <- donnees_datatable[, list(variable)] ; class(id)
variable <- c("identifiant","duree")
requete_duckdb %>% select(any_of(variable))

6.4 Sauf certaines variables

%let var = identifiant Sexe note_contenu;
data Colonnes;set donnees_sas (drop = &var.);run;
variable <- c("identifiant", "sexe", "note_contenu")
# Ne fonctionne pas !
#exclusion_var <- donnees_rbase[, -c(variable)]
exclusion_var <- donnees_rbase[, setdiff(names(donnees_rbase), variable)]
variable <- c("identifiant", "sexe", "note_contenu")
exclusion_var <- donnees_tidyverse %>% select(!all_of(variable))
exclusion_var <- donnees_tidyverse %>% select(-all_of(variable))
variable <- c("identifiant", "sexe", "note_contenu")
exclusion_var <- donnees_datatable[, !..variable]

Les opérateurs - et ! fonctionnent.

requete_duckdb %>% select(!identifiant)
requete_duckdb %>% select(-all_of(variable))

6.5 Sélectionner la 3e colonne

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " "
  from Var
  where varnum = 3;
run;
data Col3;set donnees_sas (keep = &nom_col.);run;
col3 <- donnees_rbase[, 3]
col3 <- donnees_rbase[3]
col3 <- donnees_tidyverse %>% pull(3)
col3 <- donnees_tidyverse %>% select(3)
col3 <- donnees_datatable[, 3]
requete_duckdb %>% select(3)

6.6 Sélectionner plusieurs colonnes

%let var = identifiant note_contenu sexe;
data Colonnes;set donnees_sas (keep = &var.);run;
proc sql;
  create table Colonnes as
  select %sysfunc(tranwrd(&var., %str( ), %str(, )))
  from donnees_sas;
quit;
cols <- c("identifiant", "note_contenu", "sexe")
colonnes <- donnees_rbase[, cols]
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs possibilités
colonnes <- donnees_tidyverse %>% select(all_of(cols))
colonnes <- donnees_tidyverse %>% select(any_of(cols))
colonnes <- donnees_tidyverse %>% select({{cols}})
colonnes <- donnees_tidyverse %>% select(!!cols)
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs écritures possibles
# Peut-être l'écriture la plus simple, à privilégier
colonnes <- donnees_datatable[, mget(cols)]
# Ecriture cohérente avec la logique data.table
colonnes <- donnees_datatable[, .SD, .SDcols = cols]
# Ecriture un peu contre-intuitve. Attention ! L'écriture est bien ..cols, et non ..(cols) !!
# Les syntaxes donnees_datatable[, ..(cols)] et donnees_datatable[, .(cols)] ne fonctionnent pas
colonnes <- donnees_datatable[, ..cols]
# Ecriture avec with = FALSE : désactive la possibilité de se référer à des colonnes sans les guillemets
# Avec with = FALSE : désactive la possibilité de se référer à des colonnes sans les guillemets
colonnes <- donnees_datatable[, cols, with = FALSE]
cols <- c("identifiant", "note_contenu", "sexe")
# Plusieurs possibilités
requete_duckdb %>% select(all_of(cols))
requete_duckdb %>% select(any_of(cols))
requete_duckdb %>% select({{cols}})
requete_duckdb %>% select(!!cols)

6.7 Sélectionner les colonnes qui débutent par le mot Note

/* 1ère solution */
data Selection_Variables;set donnees_sas (keep = Note:);run;
/* 2e solution */
proc contents data = donnees_sas out = Var noprint;run;
proc sql;
  select name into :var_notes separated by " "
  from Var where substr(upcase(name), 1, 4) = "NOTE" order by varnum;
run;
proc datasets lib = Work nolist;delete Var;run;
data donnees_sas_Notes;set donnees_sas (keep = &var_notes.);run;
varNotes <- donnees_rbase[grepl("^note", names(donnees_rbase))]
varNotes <- donnees_rbase[substr(tolower(names(donnees_rbase)), 1, 4) == "note"]
varNotes <- donnees_tidyverse %>% select(starts_with("note"))
# 1ère méthode
cols <- names(donnees_datatable)[substr(names(donnees_datatable), 1, 4) == "note"]
sel <- donnees_datatable[, .SD, .SDcols = cols]
# 2e méthode
sel <- donnees_datatable[, .SD, .SDcols = patterns("^note")]
requete_duckdb %>% select(starts_with("note"))

6.8 Sélectionner les colonnes qui ne débutent pas par le mot Note

data Selection_Variables;set donnees_sas (drop = Note:);run;
varNotes <- donnees_rbase[! grepl("^note", names(donnees_rbase))]
varNotes <- donnees_rbase[substr(tolower(names(donnees_rbase)), 1, 4) != "note"]
varNotes <- donnees_tidyverse %>% select(-starts_with("note"))
varNotes <- donnees_tidyverse %>% select(!starts_with("note"))
cols <- names(donnees_datatable)[substr(names(donnees_datatable), 1, 4) == "note"]
sel <- donnees_datatable[, .SD, .SDcols = -cols]
sel <- donnees_datatable[, .SD, .SDcols = -patterns("^note")]
requete_duckdb %>% select(-starts_with("note"))
requete_duckdb %>% select(!starts_with("note"))

6.9 Sélectionner l’ensemble des variables numériques de la base

data Colonnes;set donnees_sas (keep = _numeric_);run;
varNumeriques <- donnees_rbase[, sapply(donnees_rbase, is.numeric), drop = FALSE]
varNumeriques <- donnees_tidyverse %>% select_if(is.numeric)
varNumeriques <- donnees_tidyverse %>% select(where(is.numeric))
sel <- donnees_datatable[, .SD, .SDcols = is.numeric]
requete_duckdb %>% select_if(is.numeric)
# requete_duckdb %>% select(where(is.numeric))

6.10 Sélectionner l’ensemble des variables de format “Date”

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " "
  from Var where format not in ("$", "");
run;
data Colonnes;set donnees_sas (keep = &nom_col.);run;
proc datasets lib = Work nolist;delete Var;run;
varDates <- donnees_rbase[, sapply(donnees_rbase, is.Date), drop = FALSE]
varDates <- Filter(is.Date, donnees_rbase)
varDates <- donnees_tidyverse %>% select_if(is.Date)
varDates <- donnees_tidyverse %>% select(where(is.Date))
var_dates <- donnees_datatable[, .SD, .SDcols = is.Date]
requete_duckdb %>% select_if(is.Date)
# requete_duckdb %>% select(where(is.Date))

6.11 Sélectionner des lignes

/* 3e ligne */
data Ligne3;set donnees_sas (firstobs = 3 obs = 3);run;

/* Sélection des 3 premières lignes et des 3 premières colonnes de la base */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " " from Var
  where 1 <= varnum <= 3;
run;
data Top3;
  set donnees_sas (firstobs = 1 obs = 3 keep = &nom_col.);
run;
proc datasets lib = Work nolist;delete Var;run;

/* Sélection de lignes */
/* Entrées en 2023 */
data En2023;
  set donnees_sas (where = (year(date_entree) = 2023));
run;
data Avant2023_femme;
  set donnees_sas (where = (year(date_entree) < 2023 and sexe = 2));
run;
# Sélection des 3 premières lignes et des 3 premières colonnes de la base
top3 <- donnees_rbase[1:3, 1:3]

# 3e ligne
ligne3 <- donnees_rbase[3, ]

# Sélection de lignes
# Entrées en 2023
# ATTENTION, solution qui ne fonctionne pas toujours bien ! En effet, les valeurs manquantes sont sélectionnées !
en2023 <- donnees_rbase[lubridate::year(donnees_rbase$date_entree) == 2023, ]
# Bonnes écritures, qui excluent les valeurs manquantes
en2023 <- donnees_rbase[lubridate::year(donnees_rbase$date_entree) %in% c(2023), ]
en2023 <- donnees_rbase[which(lubridate::year(donnees_rbase$date_entree) == 2023), ]
en2023 <- subset(donnees_rbase, lubridate::year(donnees_rbase$date_entree) == 2023)
# Sélection des 3 premières lignes et des 3 premières colonnes de la base
top3 <- donnees_tidyverse %>% slice(1:3) %>% select(1:3)

# 3e ligne
ligne3 <- donnees_tidyverse %>% slice(3)

# Sélection de lignes
# Entrées en 2023
en2023 <- donnees_tidyverse %>% filter(lubridate::year(date_entree) == 2023)
top3 <- donnees_datatable[1:3, 1:3]

# 3e ligne
ligne3 <- donnees_datatable[3, ]
ligne3 <- donnees_datatable[3]

# Sélection de lignes
# Entrées en 2023
# Pas de problème avec les valeurs manquantes comme pour la syntaxe en R-Base
en2023 <- donnees_datatable[lubridate::year(date_entree) == 2023, ]
en2023 <- donnees_datatable[lubridate::year(date_entree) == 2023]
en2023 <- subset(donnees_datatable, lubridate::year(date_entree) == 2023)

DuckDB, moteur SQL, ne respecte pas l’ordre des lignes. Il faut passer par un filtre ou choisir explicitement un ordre.

requete_duckdb %>% filter(lubridate::year(date_entree) == 2023)

6.12 Sélection sur de multiples conditions

/* Ecriture correcte */
data Avant2023_Femme;
  set donnees_sas (where = (year(date_entree) < 2023 and not missing(date_entree) and sexe = 2));
run;

/* Ecriture incorrecte. Les valeurs manquantes sont considérées comme des nombres négatifs faibles, et inférieurs à 2023. */
/* Ils sont sélectionnés dans le code suivant : */
data Avant2023_Femme;
  set donnees_sas (where = (year(date_entree) < 2023 and sexe = 2));
run;
avant2023_femme <- subset(donnees_rbase, lubridate::year(date_entree) < 2023 & sexe == "2")
avant2023_femme <- donnees_tidyverse %>% 
  filter(lubridate::year(date_entree) < 2023 & sexe == "2")
avant2023_femme <- donnees_datatable[lubridate::year(date_entree) < 2023 & sexe == "2"]
avant2023_femme <- subset(donnees_datatable, lubridate::year(date_entree) < 2023 & sexe == "2")
requete_duckdb %>% 
  filter(lubridate::year(date_entree) < 2023 & sexe == "2")

6.13 Sélection de ligne par référence : lignes de l’identifiant 087

%let var = identifiant;
%let sel = 087;
data Selection;
  set donnees_sas;
  if &var. in ("&sel.");
run;
variable <- "identifiant"
sel <- "087"
donnees_rbase[donnees_rbase[, variable] %in% sel, ]
donnees_tidyverse %>% filter(identifiant %in% c("087")) %>% select(identifiant)
donnees_tidyverse %>% filter(identifiant == "087") %>% select(identifiant)

# Essayons désormais par variable
variable <- "identifiant"
sel <- "087"
# À FAIRE : peut-on faire plus simplement ?
donnees_tidyverse %>% filter(get(variable) %in% sel) %>% select(all_of(variable))
variable <- "identifiant"
sel <- "087"
donnees_datatable[donnees_datatable[[variable]] %chin% sel, ]
donnees_datatable[donnees_datatable[[variable]] %chin% sel, ]
requete_duckdb %>% filter(identifiant %in% c("087")) %>% select(identifiant)
requete_duckdb %>% filter(identifiant == "087") %>% select(identifiant)
# Essayons désormais par variables
variable <- "identifiant"
sel <- "087"
# À FAIRE : peut-on faire plus simplement ?
requete_duckdb %>% filter(.data[[variable]] %in% sel) %>% select(all_of(variable))

6.14 Sélection de lignes et de variables

%let cols = identifiant note_contenu sexe;
data Femmes;
  set donnees_sas (where = (Sexe = 2) keep = &cols.);
run;
data Femmes;
  set donnees_sas;
  if Sexe = 2;
  keep &cols.;
run;

/* Par nom ou par variable */
%let var = identifiant Sexe note_contenu;
data Femmes;
  /* Sélection de colonnes */
  set donnees_sas (keep = &var.);
  /* Sélection de lignes respectant une certaine condition */
  if Sexe = "2";
  /* Création de colonne */
  note2 = note_contenu / 20 * 5;
  /* Suppression de colonnes */
  drop Sexe;
  /* Selection de colonnes */
  keep identifiant Sexe note_contenu;
run;
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_rbase[donnees_rbase$sexe %in% c("2"), cols]
femmes <- subset(donnees_rbase, sexe %in% c("2"), cols)
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_tidyverse %>% filter(sexe == "2") %>% select(all_of(cols))
femmes <- donnees_tidyverse %>% filter(sexe == "2") %>% select({{cols}})
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
femmes <- donnees_datatable[sexe == "2", ..cols]
femmes <- subset(donnees_datatable, sexe %in% c("2"), cols)
cols <- c("identifiant", "note_contenu", "sexe", "date_naissance")
requete_duckdb %>% filter(sexe == "2") %>% select(all_of(cols))
requete_duckdb %>% filter(sexe == "2") %>% select({{cols}})

7 Manipuler des lignes et des colonnes

7.1 Renommer des variables

data donnees_sas;
  set donnees_sas (rename = (sexe = sexe2));
  rename sexe2 = sexe;
run;
# On renomme la variable sexe en sexe_red
names(donnees_rbase)[names(donnees_rbase) == "sexe"] <- "sexe_red"
# On la renomme en sexe
names(donnees_rbase)[names(donnees_rbase) == "sexe_red"] <- "sexe"
# On renomme la variable sexe en sexe_red
donnees_tidyverse <- donnees_tidyverse %>%
  rename(sexe_red = sexe)
# On la renomme en sexe
donnees_tidyverse <- donnees_tidyverse %>%
  rename(sexe = sexe_red)
# On renomme la variable sexe en sexe_red
names(donnees_datatable)[names(donnees_datatable) == "sexe"] <- "sexe_red"
# On la renomme en sexe
names(donnees_datatable)[names(donnees_datatable) == "sexe_red"] <- "sexe"
# Autre solution
# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setnames(donnees_datatable, "sexe", "sexe_red")
setnames(donnees_datatable, "sexe_red", "sexe")
# On renomme la variable sexe en sexe_red
requete_duckdb %>% rename(sexe_red = sexe)
# On la renomme en sexe
requete_duckdb %>% rename(sexe = sexe_red)

7.2 Formater les modalités des valeurs

/* Utilisation des formats */
proc format;
  /* Variable discrète */
  value sexef
  1 = "Homme"
  2 = "Femme";

  /* Variable continue */
  value agef
  low-<26 = "1. De 15 à 25 ans"
  26<-<50 = "2. De 26 à 49 ans"
  50-high = "3. 50 ans ou plus";

  /* Variable caractère */
  value $ cspf
  '1' = "Cadre"
  '2' = "Profession intermédiaire"
  '3' = "Employé"
  '4' = "Ouvrier"
  '5' = "Retraité";
run;
sexef <- c("1" = "Homme", "2" = "Femme")
cspf <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")
sexef_format <- c("1" = "Homme", "2" = "Femme")
cspf_format <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")
sexeform <- c("1" = "Homme", "2" = "Femme")
cspform <- c("1" = "Cadre", "2" = "Profession intermédiaire", "3" = "Employé", "4" = "Ouvrier", "5" = "Retraité")

Préférer case_match quand il s’agit de valeurs déterminées.

requete_duckdb %>% 
  mutate(sexef = case_when(
    sexef=="1" ~ "Homme",
    sexef=="2" ~ "Femme",
    .default = sexef),
         cspf = case_match(csp,
    "1" ~ "Cadre",
    "2" ~ "Profession intermédiaire",
    "3" ~ "Employé",
    "4" ~ "Ouvrier",
    "5" ~ "Retraité",
    .default = csp)) %>% 
  select(Sexe, sexef, csp, cspf)

7.3 Utiliser les formats

data donnees_sas;
  set donnees_sas;
  /* Exprimer dans le format sexef (Hommes / Femmes) */
  format Sexef $25.;
  Sexef = put(Sexe, sexef.);
  /* On exprime la CSP en texte dans une variable CSPF avec le format */
  format CSPF $25.;
  CSPF = put(CSP, $cspf.);
run;
# On exprime CSP et sexe en formaté
donnees_rbase$cspf <- cspf[donnees_rbase$csp]
donnees_rbase$sexef <- sexef[donnees_rbase$sexe]
# On exprime CSP et sexe en formaté
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(sexef = sexef_format[sexe],
         cspf = cspf_format[csp])

# Autre solution
# Les éventuelles valeurs manquantes sont conservées en NA
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(
    sexef = case_when(
      sexe == "1" ~ "Homme",
      sexe == "2" ~ "Femme",
      TRUE        ~ sexe),
    cspf = case_when(
      csp == "1" ~ "Cadre",
      csp == "2" ~ "Profession intermédiaire",
      csp == "3" ~ "Employé",
      csp == "4" ~ "Ouvrier",
      csp == "5" ~ "Retraité",
      TRUE       ~ csp)
    )
# Syntaxe pour attribuer une valeur aux NA
valeurAuxNA <- donnees_tidyverse %>% 
  mutate(sexef = case_when(
    sexe == "1" ~ "Homme",
    sexe == "2" ~ "Femme",
    is.na(x)    ~ "Inconnu",
    TRUE        ~ sexe))
# On exprime CSP et sexe en formaté
donnees_datatable[, `:=` (cspf = cspform[csp], sexef = sexeform[sexe])]

7.4 Transformer le format d’une variable

data donnees_sas;
  set donnees_sas;
  /* Transformer la variable Sexe en caractère */
  Sexe_car = put(Sexe, $1.);
  /* Transformer la variable Sexe_car en numérique */
  Sexe_num = input(Sexe_car, 1.);
  /* Transformer une date d'un format caractère à un format Date */
  format date $10.;
  date = "01/01/2000";
  format date_sas yymmdd10.;
  date_sas = input(date, ddmmyy10.);
run;
# Transformer la variable sexe en numérique
donnees_rbase$sexe_numerique <- as.numeric(donnees_rbase$sexe)
# Transformer la variable sexe_numerique en caractère
donnees_rbase$sexe_caractere <- as.character(donnees_rbase$sexe_numerique)
# Transformer une date d'un format caractère à un format Date
donnees_rbase$date_r <- lubridate::dmy("01/01/2000")
# Transformer la variable sexe en numérique
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(sexe_numerique = as.numeric(sexe))

# Transformer la variable sexe_numerique en caractère
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(sexe_caractere = as.character(sexe_numerique))

# Transformer une date d'un format caractère à un format Date
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(date_r = lubridate::dmy("01/01/2000"))
# Transformer la variable sexe en numérique
donnees_datatable[, sexe_numerique := as.numeric(sexe)]
# Transformer la variable sexe_numerique en caractère
donnees_datatable[, sexe_caractere := as.numeric(sexe_numerique)]
# Transformer une date d'un format caractère à un format Date
donnees_datatable[, date_r := lubridate::dmy("01/01/2000")]
# À FAIRE
#enNumerique <- c("Duree", "Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel")
#enDate <- c('Date_naissance', 'Date_entree')
#
#requete_duckdb %>%  
#  mutate_at(enNumerique, as.integer) %>% 
#  mutate(poids_sondage=as.numeric(poids_sondage)) %>%
#  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
#  select(enDate, Duree, Note_Contenu)
#

Note : duckdb fait des conversions de type implicitement, mais seulement les conversions incontestables. Il faudra souvent préciser le type des variables.

7.5 Création et suppressions de plusieurs variables

/* Manipulation de colonnes par référence */
data Creation;
  set donnees_sas;
  note_contenu2 = note_contenu / 20 * 5;
  note_formateur2 = note_formateur / 20 * 5;
  /* Suppression des variables créées */
  drop note_contenu2 note_formateur2;
run;

/* Par nom ou par variable */
%let var = identifiant Sexe note_contenu;
data Femmes;
  /* Sélection de colonnes */
  set donnees_sas (keep = &var.);
  /* Sélection de lignes respectant une certaine condition */
  if Sexe = "2";
  /* Création de colonne */
  note2 = note_contenu / 20 * 5;
  /* Suppression de colonnes */
  drop note2;
  /* Selection de colonnes */
  keep identifiant Sexe note_contenu;
run;
donnees_rbase$note2 <- donnees_rbase$note_contenu / 20 * 5
# Le with permet de s'affranchir des expressions "donnees_rbase$"
with(donnees_rbase, note2 <- note_contenu / 20 * 5)
donnees_rbase <- transform(donnees_rbase, note2 = note_contenu / 20 * 5)
# On ne peut pas utiliser transform pour des variables récemment créées
#donnees_rbase <- transform(donnees_rbase, note3 = note_contenu ** 2, note3 = log(note3))

# Suppression de variables
donnees_rbase$note2 <- NULL

# Création et suppressions de plusieurs variables
donnees_rbase <- transform(donnees_rbase, note_contenu2 = note_contenu / 20 * 5, note_formateur2 = note_formateur / 20 * 5)
# Suppression des variables créées
variable <- c("note_contenu2", "note_formateur2")
donnees_rbase[, variable] <- NULL
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(note2 = note_contenu / 20 * 5)

# Suppression de variables
donnees_tidyverse <- donnees_tidyverse %>% 
  select(-note2)

# Création et suppressions de plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(note_contenu2 = note_contenu / 20 * 5,
         note_formateur2 = note_formateur / 20 * 5)

# Suppression des variables créées
variable <- c("note_contenu2", "note_formateur2")
donnees_tidyverse <- donnees_tidyverse %>% 
  select(-all_of(variable))
# Création de variables
donnees_datatable[, note2 := note_contenu / 20 * 5]

# Suppression de variables
donnees_datatable[, note2 := NULL]

# Création et suppressions de plusieurs variables
donnees_datatable[, c("note_contenu2", "note_formateur2") := list(note_contenu / 20 * 5, note_formateur / 20 * 5)]
donnees_datatable[, `:=` (note_contenu2 = note_contenu / 20 * 5, note_formateur2 = note_formateur / 20 * 5)]
# Suppression des variables créées
donnees_datatable[, c("note_contenu2", "note_formateur2") := NULL]
# Ou par référence extérieure
variable <- c("note_contenu2", "note_formateur2")
donnees_datatable[, `:=` (note_contenu2 = note_contenu / 20 * 5, note_formateur2 = note_formateur / 20 * 5)]
donnees_datatable[, (variable) := NULL]
# À FAIRE : à compléter !
# Création de la colonne note2
requete_duckdb %>% 
  mutate(note2 = as.integer(Note_Contenu) / 20 * 5) %>% 
  select(note2)

# Suppression de colonnes
#requete_duckdb %>% select(- CSP, -contains("Date"), -starts_with("Note"))

7.6 On souhaite réexprimer toutes les notes sur 100 et non sur 20

%let notes = Note_Contenu   Note_Formateur Note_Moyens     Note_Accompagnement     Note_Materiel;
/* On supprime d'abord les doubles blancs entre les variables */
%let notes = %sysfunc(compbl(&notes.));
/* on affiche les notes dans la log de SAS */
%put &notes;
/* 1ère solution : avec les array */
/* Les variables sont modifiées dans cet exemple */
data Sur100_1;
  set donnees_sas;
  array variables (*) &notes.;
  do increment = 1 to dim(variables);
    variables[increment] = variables[increment] / 20 * 100;
  end; 
  drop increment;
run;
/* 2e solution : avec une macro */
/* De nouvelles variables sont ajoutées dans cet exemple */
data donnees_sas;
  set donnees_sas;
  %macro Sur100;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let note = %scan(&notes., &i.);
      &note._100 = &note. / 20 * 100;
    %end;
  %mend Sur100;
  %Sur100;
run;
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
notes <- names(donnees_rbase)[grepl("^note", names(donnees_rbase))]
# Les variables sont modifiées dans cet exemple
sur100 <- donnees_rbase[, notes] / 20 * 100
# On  souhaite conserver les notes sur 100 dans d'autres variables, suffixées par _100
donnees_rbase[, paste0(notes, "_100")] <- donnees_rbase[, notes] / 20 * 100
# Les variables sont modifiées dans cet exemple
sur100 <- donnees_tidyverse %>% 
  mutate(across(starts_with("note"), ~ .x / 20 * 100))

# On  souhaite conserver les notes sur 100 dans d'autres variables, suffixées par _100
notes <- names(donnees_tidyverse)[grepl("^note", names(donnees_tidyverse))]
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(across(starts_with("note"), ~ .x / 20 * 100, .names = "{.col}_100"))
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
notes <- names(donnees_datatable)[grepl("^note", names(donnees_datatable))]
# Les variables sont modifiées dans cet exemple
sur100 <- copy(donnees_datatable)
sur100 <- sur100[, (notes) := lapply(.SD, function(x) x / 20 * 100), .SDcols = notes]
sur100 <- sur100[, (notes) := lapply(.SD, function(x) x / 20 * 100), .SD = notes]
# Ou encore, plus simple
# Dans cet exemple, les notes dans la base donnees_datatable ne sont pas changées
sur100 <- sur100[, lapply(.SD, function(x) x / 20 * 100), .SDcols = patterns("^note")]
# On  souhaite conserver les notes sur 20 dans d'autres variables, suffixées par _20
donnees_datatable[, (paste0(notes, "_100")) := lapply(.SD, function(x) x / 20 * 100), .SDcols = notes]
requete_duckdb %>% 
  mutate(across(starts_with("note"), ~ as.numeric(.x)/20*100)) %>% 
  select(starts_with("note"))

7.7 Création de variables avec des conditions

data Civilite;
  set donnees_sas;
  /* 1ère solution */
  format Civilite $20.;
  if      Sexe = 2 then Civilite = "Mme";
  else if Sexe = 1 then Civilite = "Mr";
  else                  Civilite = "Inconnu";
  /* 2e solution (do - end) */
  if      Sexe = 2 then do;
    Civilite2 = "Femme";
  end;
  else if Sexe = 1 then do;
    Civilite2 = "Homme";
  end;
  else do;
    Civilite2 = "Inconnu";
  end;
  /* 3e solution */
  format Civilite3 $20.;
  select;
    when      (Sexe = 2) Civilite3 = "Femme";
    when      (Sexe = 1) Civilite3 = "Homme";
    otherwise            Civilite3 = "Inconnu";
  end;
  keep Sexe Civilite Civilite2 Civilite3;run;
run;
donnees_rbase$civilite <- ifelse(donnees_rbase$sexe == "2", "Mme", 
                           ifelse(donnees_rbase$sexe == "1", "M", 
                                  "Inconnu"))
# Autre solution
donnees_rbase$civilite <- "Inconnu"
donnees_rbase$civilite[donnees_rbase$sexe == "1"] <- "M"
donnees_rbase$civilite[donnees_rbase$sexe == "2"] <- "Mme"
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(civilite = case_when(sexe == "2" ~ "Mme",
                              sexe == "1" ~ "M",
                              TRUE        ~ "Inconnu")
)
donnees_datatable[, civilite := fcase(sexe == "2", "Mme",
                                      sexe == "1", "M.",
                                      is.na(sexe), "Inconnu")]

7.8 Manipuler les dates

/* On utilise ici %sysevalf et non %eval pour des calculs avec des macro-variables non entières */
%let sixmois = %sysevalf(365.25/2);
%put sixmois : &sixmois.;
data donnees_sas;
  set donnees_sas;
  /* Âge à l'entrée dans le dispositif */
  Age = intck('year', date_naissance, date_entree);
  /* Âge formaté */
  Agef = put(Age, agef.);
  /* Date de sortie du dispositif : ajout de la durée à la date d'entrée */
  format date_sortie ddmmyy10.;
  date_sortie = intnx('day', date_entree, duree);  
  /* La durée du contrat est-elle inférieure à 6 mois ? */
  Duree_Inf_6_mois = (Duree < &sixmois. & Duree ne .);
  /* Deux manières de créer une date */
  format Decembre_31_&an._a Decembre_31_&an._b ddmmyy10.;
  Decembre_31_&an._a = "31dec&an."d;
  /* mdy pour month, day, year (pas d'autre alternative, ymd par exemple n'existe pas) */
  Decembre_31_&an._b = mdy(12, 31, &an.); 
  /* Date 6 mois après la sortie */
  format Date_6mois ddmmyy10.;
  Date_6mois = intnx('month', date_sortie, 6);
run;
/* Ventilation pondérée (cf. infra) */
proc freq data = donnees_sas;tables apres_31_decembre;weight poids_sondage;run;
# Âge à l'entrée dans le dispositif
donnees_rbase$age <- floor(lubridate::time_length(difftime(donnees_rbase$date_entree, donnees_rbase$date_naissance), "years"))
# Âge formaté
donnees_rbase$agef[donnees_rbase$age < 26]                           <- "1. De 15 à 25 ans"
# 26 <= donnees_rbase$age < 50 ne fonctionne pas, il faut passer en 2 étapes
donnees_rbase$agef[26 <= donnees_rbase$age & donnees_rbase$age < 50] <- "2. De 26 à 49 ans"
donnees_rbase$agef[donnees_rbase$age >= 50]                          <- "3. 50 ans ou plus"
# Autre solution
# L'option right = TRUE implique que les bornes sont ]0; 25] / ]25; 49] / ]49; Infini[
agef <- cut(donnees_rbase$age, 
            breaks = c(0, 25, 49, Inf),
            right = TRUE,
            labels = c("1. De 15 à 25 ans", "2. De 26 à 49 ans", "3. 50 ans ou plus"), 
            ordered_result = TRUE)

# Manipuler les dates
sixmois <- 365.25/2
# La durée du contrat est-elle inférieure à 6 mois ?
donnees_rbase$duree_inf_6_mois <- ifelse(donnees_rbase$duree < sixmois, 1, 0)
# Date de sortie du dispositif
donnees_rbase$date_sortie <- donnees_rbase$date_entree + lubridate::days(donnees_rbase$duree)

# Pour créer une date
as.Date(paste0(annee,"-12-31"), origin = "1970-01-01")
lubridate::ymd(paste0(annee,"-12-31"))

# Date 6 mois après la sortie
donnees_rbase$date_6mois <- donnees_rbase$date_sortie + lubridate::month(6)
# Âge à l'entrée dans le dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(age = as.period(interval(start = date_naissance, end = date_entree))$year)
# Âge formaté
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(agef = case_when(
    age < 26             ~ "1. De 15 à 25 ans",
    age >= 26 & age < 50 ~ "2. De 26 à 49 ans",
    age >= 50            ~ "3. 50 ans ou plus")
    )


# Manipuler les dates
sixmois <- 365.25/2
# La durée du contrat est-elle inférieure à 6 mois ?
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(duree_inf_6_mois = case_when(duree <  sixmois ~ 1,
                                      duree >= sixmois ~ 0))
donnees_tidyverse %>% pull(duree_inf_6_mois) %>% table()

# Date de sortie du dispositif
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_sortie = date_entree + lubridate::days(duree))

# Pour créer une date
as.Date(paste0(annee,"-12-31"))
lubridate::ymd(paste0(annee,"-12-31"))

# Date 6 mois après la sortie
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(date_6mois = date_sortie + lubridate::month(6))
# Âge à l'entrée dans le dispositif
donnees_datatable[, age := floor(lubridate::time_length(difftime(donnees_datatable$date_entree, donnees_datatable$date_naissance), "years"))]

# Âge formaté
donnees_datatable[, agef := fcase(age < 26,             "1. De 15 à 25 ans",
                                  26 <= age & age < 50, "2. De 26 à 49 ans",
                                  age >= 50,            "3. 50 ans ou plus")]

# Manipuler les dates
sixmois <- 365.25/2
# La durée du contrat est-elle inférieure à 6 mois ?
donnees_datatable[, duree_inf_6_mois := ifelse(duree >= sixmois, 1, 0)]
donnees_datatable[, duree_inf_6_mois := fifelse(duree >= sixmois, 1, 0)]
donnees_datatable[, duree_inf_6_mois := fcase(duree >= sixmois, 1,
                                              duree <  sixmois, 0)]
# Date de sortie du dispositif
donnees_datatable[, date_sortie := date_entree + lubridate::days(duree)]

# Pour créer une date
as.Date(paste0(annee,"-12-31"))
lubridate::ymd(paste0(annee,"-12-31"))

# Date 6 mois après la sortie
donnees_datatable[, date_6mois := date_sortie + lubridate::month(6)]
# Création de la colonne age 
requete_duckdb %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  mutate(age = year(age(Date_entree,Date_naissance))) %>% 
  select(age)

# Âge formaté
requete_duckdb %>%
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  mutate(age = year(age(Date_entree,Date_naissance))) %>% 
  mutate(agef = case_when(
    age < 26 ~ "1. De 15 à 25 ans",
    age >= 26 | age < 50 ~  "2. De 26 à 49 ans",
    age >= 50 ~ "3. 50 ans ou plus")) %>% 
  select(age, agef)

7.9 Mettre un 0 devant un nombre

data Zero_devant;
  set donnees_sas (keep = date_entree);
  /* Obtenir le mois et la date */
  Mois = month(date_entree);
  Annee = year(date_entree);
  /* Mettre le mois sur 2 positions (avec un 0 devant si le mois <= 9) : format prédéfini z2. */
  Mois_a = put(Mois, z2.);
  drop Mois;
  rename Mois_a = Mois;
run;
# Obtenir le mois et la date
donnees_rbase$mois <- lubridate::month(donnees_rbase$date_entree)
donnees_rbase$annee <- lubridate::year(donnees_rbase$date_entree)
# Mettre le numéro du mois sur 2 positions (avec un 0 devant si le mois <= 9)
donnees_rbase$mois <- sprintf("%02d", donnees_rbase$mois)
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(mois = sprintf("%02d", lubridate::month(date_entree)))
# Autre solution
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(mois = lubridate::month(date_entree),
         mois = ifelse(str_length(mois) < 2, paste0("0", mois), mois))
# Obtenir le mois et la date
donnees_datatable[, `:=`(mois = lubridate::month(date_entree), annee = lubridate::year(donnees_datatable$date_entree))]
# Mettre le numéro du mois sur 2 positions (avec un 0 devant si le mois <= 9)
donnees_datatable[, mois := sprintf("%02d", mois)]
# À FAIRE : pas exactement au bon endroit
#requete_duckdb %>% 
#  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
#  mutate(mois=lubridate::month(Date_entree),
#         mois=ifelse(str_length(mois)<2, paste0("0", mois), mois)) %>% 
#  select(mois, Date_entree)

7.10 Arrondir une valeur numérique

data Arrondis;
  set donnees_sas (keep = poids_sondage);
  /* Arrondi à l'entier le plus proche */
  poids_arrondi_0 = round(poids_sondage);
  /* Arrondi à 1 chiffre après la virgule */
  poids_arrondi_1 = round(poids_sondage, 0.1);
  /* Arrondi à 2 chiffre après la virgule */
  poids_arrondi_2 = round(poids_sondage, 0.2);
  /* Arrondi à l'entier inférieur */
  poids_inf = floor(poids_sondage);
  /* Arrondi à l'entier supérieur */
  poids_sup = ceil(poids_sondage);  
run;
# Arrondi à l'entier le plus proche
poids_arrondi_0 <- round(donnees_rbase$poids_sondage, 0)
# Arrondi à 1 chiffre après la virgule
poids_arrondi_1 <- round(donnees_rbase$poids_sondage, 1)
# Arrondi à 2 chiffre après la virgule
poids_arrondi_2 <- round(donnees_rbase$poids_sondage, 2)
# Arrondi à l'entier inférieur
poids_inf <- floor(donnees_rbase$poids_sondage)
# Arrondi à l'entier supérieur
poids_sup <- ceiling(donnees_rbase$poids_sondage)
donnees_tidyverse <- donnees_tidyverse %>% 
  # Arrondi à l'entier le plus proche
  mutate(poids_arrondi_0 = round(poids_sondage, 0)) %>% 
  # Arrondi à 1 chiffre après la virgule
  mutate(poids_arrondi_1 = round(poids_sondage, 1)) %>% 
  # Arrondi à 2 chiffre après la virgule
  mutate(poids_arrondi_2 = round(poids_sondage, 2)) %>% 
  # Arrondi à l'entier inférieur
  mutate(poids_inf = floor(poids_sondage)) %>% 
  # Arrondi à l'entier supérieur
  mutate(poids_sup = ceiling(poids_sondage))
donnees_tidyverse %>% select(starts_with("poids"))
# Arrondi à l'entier le plus proche
donnees_datatable[, poids_arrondi_0 := round(poids_sondage, 0)]
# Arrondi à 1 chiffre après la virgule
donnees_datatable[, poids_arrondi_1 := round(poids_sondage, 1)]
# Arrondi à 2 chiffre après la virgule
donnees_datatable[, poids_arrondi_2 := round(poids_sondage, 2)]
# Arrondi à l'entier inférieur
donnees_datatable[, poids_inf := floor(poids_sondage)]
# Arrondi à l'entier supérieur
donnees_datatable[, poids_sup := ceiling(poids_sondage)]
requete_duckdb %>% 
  mutate( # la fonction round de duckdb ne prend pas l'argument digits, mais la traduction fonctionne
    poids_arrondi_0 = round(as.numeric(poids_sondage),0),
    poids_arrondi_1 = round(as.numeric(poids_sondage),1),
    poids_arrondi_2 = round(as.numeric(poids_sondage),-1),
    poids_floor = floor(as.numeric(poids_sondage)),
    poids_ceiling = ceiling(as.numeric(poids_sondage)),
    ) %>% 
  select(starts_with("poids"))

8 Les tris

8.1 Trier les colonnes de la base

/* On met identifiant, date_entree au début de la base */
%let colTri = identifiant date_entree;
data donnees_sas;
  retain &colTri.;
  set donnees_sas;
run;

/* Autre solution */
proc sql;
  create table donnees_sas as
  /* Dans la proc SQL, les variables doivent être séparées par des virgules */
  /* On remplace les blancs entre les mots par des virgules pour la proc SQL */
  select %sysfunc(tranwrd(&colTri., %str( ), %str(, ))), * from donnees_sas;
quit;

/* Mettre la variable poids_sondage au début de la base */
data donnees_sas;
  retain poids_sondage;
  set donnees_sas;
run;

/* Mettre la variable poids_sondage à la fin de la base */
proc contents data = donnees_sas out = var;run;
proc sql noprint;
  select name into :var separated by " " from var
  where lowcase(name) ne "poids_sondage" order by varnum;
quit;
data donnees_sas;
  retain &var. poids_sondage;
  set donnees_sas;
run;
# Mettre les variables identifiant, date_entree au début de la base
colTri <- c("identifiant", "date_entree")
donnees_rbase <- donnees_rbase[, union(colTri, colnames(donnees_rbase))]

# Autre possibilité, plus longue !
donnees_rbase <- donnees_rbase[, c(colTri, setdiff(colnames(donnees_rbase), colTri))]
donnees_rbase <- donnees_rbase[, c(colTri, colnames(donnees_rbase)[! colnames(donnees_rbase) %in% colTri])]

# Mettre la variable poids_sondage au début de la base
donnees_rbase <- donnees_rbase[, c("poids_sondage", setdiff(colnames(donnees_rbase), "poids_sondage"))]
# Mettre la variable poids_sondage à la fin de la base
donnees_rbase <- donnees_rbase[, c(setdiff(colnames(donnees_rbase), "poids_sondage"), "poids_sondage")]
# Mettre les variables identifiant, date_entree et date_sortie au début de la base
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(identifiant, date_entree)

# Autres solutions
colTri <- c("identifiant", "date_entree")
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(all_of(colTri))
donnees_tidyverse_tri <- donnees_tidyverse %>% 
  select(all_of(colTri), everything())

# Mettre la variable poids_sondage au début de la base
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(poids_sondage)
# Mettre la variable poids_sondage à la fin de la base
donnees_tidyverse <- donnees_tidyverse %>%
  relocate(poids_sondage, .after = last_col())
# On met identifiant, date_entree au début
colTri <- c("identifiant", "date_entree")
tri <- union(colTri, colnames(donnees_datatable))
donnees_datatable <- donnees_datatable[, ..tri]

# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setcolorder(donnees_datatable, colTri)

# Mettre la variable poids_sondage au début de la base
setcolorder(donnees_datatable, union("poids_sondage", colnames(donnees_datatable)))
# Mettre la variable poids_sondage à la fin de la base
setcolorder(donnees_datatable, c(setdiff(colnames(donnees_datatable), "poids_sondage"), "poids_sondage"))
# On met identifiant date_entree au début
requete_duckdb %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  select(identifiant, date_entree, everything())

requete_duckdb %>% 
  mutate_at(enDate, ~ as.Date(strptime(.,'%d/%m/%Y'))) %>% # strptime est une fonction duckdb
  relocate(identifiant, date_entree)

8.2 Trier les lignes (par ordre croissant ou décroissant)

/* 1ère possibilité */
proc sort data = donnees_sas;by Identifiant Date_entree;run;

/* 2e possibilité */
proc sql;
  create table Donnes as select * from donnees_sas
  order by Identifiant, Date_entree;
quit;

/* Idem par ordre croissant d'identifiant et ordre décroissant de date d'entrée */
proc sort data = donnees_sas;by Identifiant descending Date_entree;run;
proc sql;
  create table Donnes as select * from donnees_sas
  order by Identifiant, Date_entree desc;
quit;
# Tri par ordre croissant
# L'option na.last = FALSE (resp. TRUE) indique que les valeurs manquantes doivent figurer à la fin (resp. au début) du tri, que le tri
# soit croissant ou décroissant
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
# Tri par ordre croissant de identifiant et décroissant de date_entree (- avant le nom de la variable)
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE, decreasing = c(FALSE, TRUE), method = "radix"), ]
# Autre possibilité : - devant la variable (uniquement pour les variables numériques)
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, -donnees_rbase$duree, na.last = FALSE), ]
# Tri par ordre croissant
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, date_entree)
# Tri par ordre croissant de identifiant et décroissant de date_entree
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))
# Tri par ordre croissant
# L'option na.last = FALSE (resp. TRUE) indique que les valeurs manquantes doivent figurer à la fin (resp. au début) du tri, que le tri
# soit croissant ou décroissant
donnees_datatable <- donnees_datatable[order(identifiant, date_entree, na.last = FALSE)]

# En data.table, les instructions débutant par set modifient les éléments par référence, c'est-à-dire sans copie.
# Ceci est plus efficace pour manipuler des données volumineuses.
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
setorder(donnees_datatable, identifiant, date_entree, na.last = FALSE)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)

# Tri par ordre croissant de identifiant et décroissant de date_entree (- avant le nom de la variable)
donnees_datatable <- donnees_datatable[order(identifiant, -date_entree, na.last = FALSE)]
setorder(donnees_datatable, "identifiant", -"date_entree", na.last = FALSE)
setorder(donnees_datatable, identifiant, -date_entree, na.last = FALSE)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, -1L), na.last = FALSE)

8.3 Incidence des valeurs manquantes dans les tris

/* Dans SAS, les valeurs manquantes sont considérées comme des valeurs négatives */
/* Elles sont donc situées en premier dans un tri par ordre croissant ... */
proc sort data = donnees_sas;by identifiant date_entree;run;proc print;run;
/* ... et en dernier dans un tri par ordre décroissant */
proc sort data = donnees_sas;by identifiant descending date_entree;run;
proc print;run;
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree), ]

# SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Pour mimer le tri par ordre croissant en SAS :
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ]
# Pour mimer le tri par ordre décroissant en SAS :
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree,
                                     na.last = FALSE,
                                     decreasing = c(FALSE, TRUE),
                                     method = "radix"), ]
# Attention, avec arrange, les variables manquantes (NA) sont toujours classées en dernier, même avec desc()
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, date_entree)
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))

# Or, SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Elles sont donc classées en premier dans un tri par ordre croissant, et en dernier dans un tri par ordre décroissant

# Pour mimer le tri par ordre croissant en SAS : les valeurs manquantes de date_entree sont classées en premier
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, !is.na(date_entree), date_entree)
# Pour mimer le tri par ordre décroissant en SAS
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(identifiant, desc(date_entree))
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
donnees_datatable <- donnees_datatable[order(identifiant, date_entree)]
# SAS considère les valeurs manquantes comme des nombres négatifs faibles.
# Pour mimer le tri par ordre croissant en SAS :
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
# Pour mimer le tri par ordre décroissant en SAS :
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, -1L), na.last = FALSE)
# Les valeurs manquantes sont situées en dernier dans un tri par ordre croissant ou décroissant (car par défaut l'option na.last = TRUE) ...
requete_duckdb %>% arrange(Identifiant, Note_Contenu) %>% select(Identifiant, Note_Contenu)
  
# Pour mimer le tri SAS, il faut écrire :
# Note : il faut faire select d'abord, sinon il y a une erreur quand "! is.na()" est dans la liste des colonnes
requete_duckdb %>% select(Identifiant, Note_Contenu) %>% arrange(Identifiant, ! is.na(Note_Contenu), Note_Contenu)

8.4 Trier par ordre croissant par toutes les variables de la base

proc sort data = donnees_sas;by _all_;run;
tri_toutes_variables <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE)]
tri_toutes_variables <- donnees_tidyverse %>% 
  arrange(pick(everything()))
tri_toutes_variables <- donnees_tidyverse %>% 
  arrange(across(everything()))
tri_toutes_variables <- setorderv(donnees_datatable, na.last = FALSE)

9 Manipuler des chaînes de caractères

9.1 Majuscule, minuscule

/* Fonction tranwrd (TRANslate WoRD) */
data donnees_sas;
  set donnees_sas;
  /* Première lettre en majuscule */
  Niveau = propcase(Niveau);
  /* En majuscule */
  CSP_majuscule = upcase(CSPF);
  /* En minuscule */
  CSP_minuscule = lowcase(CSPF);
  /* Nombre de caractères dans une chaîne de caractères */
  taille_id = length(identifiant);
run;
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_rbase$niveau <- paste0(toupper(substr(donnees_rbase$niveau, 1, 1)), tolower(substr(donnees_rbase$niveau, 2, length(donnees_rbase$niveau))))

# En majuscule
donnees_rbase$csp_maj <- toupper(donnees_rbase$cspf)
# En minuscule
donnees_rbase$csp_min <- tolower(donnees_rbase$cspf)
# Nombre de caractères dans une chaîne de caractères
donnees_rbase$taille_id <- nchar(donnees_rbase$identifiant)
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_tidyverse <- donnees_tidyverse %>%  
  mutate(niveau = str_to_title(niveau))

# En majuscule
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(csp_maj = toupper(cspf))
# En minuscule
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(csp_maj = tolower(cspf))
# Nombre de caractères dans une chaîne de caractères
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(taille_id = nchar(identifiant))
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(taille_id = str_split(identifiant, '') %>% 
           lengths)
# 1ère lettre en majuscule, autres lettres en minuscule
donnees_datatable[, niveau := paste0(toupper(substr(niveau, 1, 1)), tolower(substr(niveau, 2, length(niveau))))]

# En majuscule
donnees_datatable[, csp_maj := toupper(cspf)]
# En minuscule
donnees_datatable[, csp_min := tolower(cspf)]
# Nombre de caractères dans une chaîne de caractères
donnees_datatable[, taille_id := nchar(identifiant)]

9.2 Remplacer une chaîne de caractères par une autre

data A_Corriger;
  infile cards dsd dlm='|';
  format A_corriger $8.;
  input A_corriger $;
  cards;
  Qualifie
  qualifie
  Qualifie
  QUALIFIE
;
run;
data A_Corriger;
  set A_Corriger;
  Corrige = lowcase(A_corriger);
  Corrige = tranwrd(Corrige, "qualifie", "Qualifié");
run;
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")
# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
gsub("[Q-q]ualifie", "Qualifié", tolower(aCorriger))
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")
# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
aCorriger %>% tolower() %>% str_replace_all("[Q-q]ualifie", "Qualifié")
# Le mot Qualifié n'a pas d'accent : on le corrige
aCorriger <- c("Qualifie", "qualifie", "Qualifie", "QUALIFIE")
# [Q-q] permet de représenter Q ou q, et donc de prendre en compte Qualifie et qualifie
gsub("[Q-q]ualifie", "Qualifié", tolower(aCorriger))

9.3 Manipuler des chaînes de caractères

data Exemple_chaines;
  Texte = "              Ce   Texte   mériterait   d être   corrigé                  ";
  Texte1 = "Je m'appelle";
  Texte2 = "SAS";
  Texte3 = "Phrase à découper";
  /* Valeur manquante sous forme caractère */
  Texte4 = "";
run;
data Exemple_chaines;
  set Exemple_chaines;
  /* Enlever les blancs au début et à la fin de la chaîne de caractère */
  Enlever_Blancs_Initiaux = strip(Texte);
  /* Enlever les doubles blancs dans la chaîne de caractères */
  Enlever_Blancs_Entre = compbl(Enlever_Blancs_Initiaux);
  /* Enlever doubles blancs */
  /* REVOIR !!!!! */
  Enlever_Doubles_Blancs = compress(Texte, "  ", "t");
  /* Trois méthodes pour concaténer des chaînes de caractères */
  Concatener  = Texte1||" "||Texte2;
  Concatener2 = Texte1!!" "!!Texte2;
  Concatener3 = catx(" ", Texte1, Texte2);
  /* Effet des valeurs manquantes */
  /* Le séparateur est enlevé lors d'une concaténation avec une chaîne de caractère vide */
  Concatener4 = catx("-", Texte4, Texte3);
  /* Extraire les 2e, 3e et 4e caractère de Concatener */
  /* 2 correspond à la position du 1er caractère à récupérer, 3 le nombre total de caractères à partir du point de départ */
  extrait = substr(Concatener, 2, 3);
  /* Transformer plusieurs caractères différents */
  /* On transforme le é en e, le â en a, le î en i, ... */
  chaine = "éèêëàâçîô";
  chaine_sans_accent = translate(chaine, "eeeeaacio", "éèêëàâçîô");
run;
proc print data = Exemple_chaines;run;
texte  <- "              Ce   Texte   mériterait   d être   corrigé                  "
texte1 <- "Ce texte"
texte2 <- "va être"
texte3 <- "concaténé"
# Valeur manquante sous forme caractère
texte4 <- ""

# Enlever les blancs au début et à la fin de la chaîne de caractère
# "\\s+" est une expression régulière indiquant 1 ou plusieurs espaces successifs
# Le gsub remplace 1 ou plusieurs espaces successifs par un seul espace
# trimws enlève les espaces au début et à la fin d'une chaîne de caractère 
texte <- gsub("\\s+", " ", trimws(texte))


# Concaténer des chaînes de caractères
concatene <- paste(texte1, texte2, texte3, sep = " ")
paste0(texte1, texte2, texte3)

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
paste(texte4, texte3, sep = "-")

# Extraire les 2e, 3e et 4e caractères de Concatener
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- substr(concatene, 2, 5)

# Transformer plusieurs caractères différents
chaine <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", chaine)
texte  <- "              Ce   Texte   mériterait   d être   corrigé                  "
texte1 <- "Ce texte"
texte2 <- "va être"
texte3 <- "concaténé"
# Valeur manquante sous forme caractère
texte4 <- ""

# Enlever les blancs au début et à la fin de la chaîne de caractère
# str_squish() supprime les espaces blancs au début et à la fin, et remplace tous les espaces blancs internes par un seul espace
texte <- str_squish(texte)

# Concaténer des chaînes de caractères
concatene <- str_flatten(c(texte1, texte2, texte3), collapse = " ")

# Effet des valeurs manquantes : le délimiteur (ici -) apparaît avec la concaténation avec le caractère manquant
str_flatten(c(texte4, texte3), collapse = "-")

# Extraire les 2e, 3e et 4e caractères de Concatener
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- str_sub(concatene, 2, 5)

# Transformer plusieurs caractères différents
chaine <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", chaine)
texte  <- "              Ce   Texte   mériterait   d être   corrigé                  "
texte1 <- "Je m'appelle"
texte2 <- "R"
# Enlever les blancs au début et à la fin de la chaîne de caractère
# "\\s+" est une expression régulière indiquant 1 ou plusieurs espaces successifs
# Le gsub remplace 1 ou plusieurs espaces successifs par un seul espace
# trimws enlève les espaces au début et à la fin d'une chaîne de caractère 
texte <- gsub("\\s+", " ", trimws(texte))
# Concaténer des chaînes de caractères
paste(texte1, texte2, sep = " ")
paste0(texte1, texte2)


# Extraire les 2e, 3e et 4e caractères de texte
# 2 correspond à la position du 1er caractère à récupérer, 5 la position du dernier caractère
extrait <- substr(texte, 2, 5)

# Transformer plusieurs caractères différents
chaine <- "éèêëàâçîô"
chartr("éèêëàâçîô", "eeeeaacio", chaine)

9.4 Découper une chaîne de caractères selon un caractère donné

/* Afficher tous les mots d'une phrase : découper une phrase selon les espaces pour isoler les mots */
data Mots;
  delim = " ";
  Texte = "Mon texte va être coupé !";
  /* Chaque mot dans une variable */
  %macro Decouper;
    %do i = 1 %to %sysfunc(countw(Texte, delim));
      Mot&i. = scan(Texte, &i., delim);
    %end;
  %mend Decouper;
  %Decouper;
  /* Les mots empilés */
  nb_mots = countw(Texte, delim);
  do nb = 1 to nb_mots;
    mots = scan(Texte, nb, delim);
    output;
  end;
run;
proc print data = Mots;run;
# Découper uen chaîne de caractères selon un caractère donné
# Afficher tous les mots d'une phrase : découper une phrase selon les espaces pour isoler les mots
chaine  <- "Mon texte va être coupé !"
unlist(strsplit(chaine, split = " "))
# Découper une chaîne de caractères selon un caractère donné
# Afficher tous les mots d'une phrase : découper une phrase selon les espaces pour isoler les mots
chaine  <- "Mon texte va être coupé !"
str_split(chaine, pattern = " ") %>% unlist()
# Découper uen chaîne de caractères selon un caractère donné
# Afficher tous les mots d'une phrase : découper une phrase selon les espaces pour isoler les mots
chaine  <- "Mon texte va être coupé !"
unlist(strsplit(chaine, split = " "))

9.5 Inverser une chaîne de caractères

data Mots;
  Texte = "Mon texte va être coupé !";
  x = left(reverse(Texte));
run;
proc print data = Mots;run;
inverserTexte <- function(x) {
  sapply(
    lapply(strsplit(x, NULL), rev),
    paste, collapse = "")
  }
inverserTexte(chaine)
library(stringi)
stringi::stri_reverse(chaine)
inverserTexte <- function(x) {
  sapply(
    lapply(strsplit(x, NULL), rev),
    paste, collapse = "")
}
inverserTexte(chaine)

10 Gestion par groupe

10.1 Numéroter les lignes

data donnees_sas;
  set donnees_sas;
  Num_observation = _n_;
run;

/* Autre solution */
proc sql noprint;select count(*) into :nbLignes from donnees_sas;quit;
data numLigne;do Num_observation = 1 to &nbLignes.;output;end;run;
data _NULL_;
  set donnees_sas nobs = n;
  call symputx('nbLignes', n);
run;
%put Nombre de lignes : &nbLignes.;

/* Le merge "simple" (sans by) va seulement concaténer les deux bases l'une à côté de l'autre */
data donnees_sas;
  merge donnees_sas numLigne;
run;
# Numéro de l'observation : 2 manières différentes
donnees_rbase$num_observation <- row.names(donnees_rbase)
donnees_rbase$num_observation <- seq(1 : nrow(donnees_rbase))

# Numéro du contrat de chaque individu, contrat trié par date de survenue
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
donnees_rbase$un <- 1
donnees_rbase$numero_contrat <- ave(donnees_rbase$un, donnees_rbase$identifiant, FUN = cumsum)
donnees_rbase$un <- NULL

# Autre solution
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
donnees_rbase$numero_contrat <- as.numeric(ave(donnees_rbase$identifiant, donnees_rbase$identifiant, FUN = seq_along))

# Autre solution : order pour éviter le as.numeric
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
donnees_rbase$numero_contrat <- ave(order(donnees_rbase$date_entree), donnees_rbase$identifiant, FUN = seq_along)
#https://stackoverflow.com/questions/11996135/create-a-sequential-number-counter-for-rows-within-each-group-of-a-dataframe
#https://stackoverflow.com/questions/13732062/what-are-examples-of-when-seq-along-works-but-seq-produces-unintended-results
# Numéro de l'observation
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(num_observation = row_number())

# Numéro du contrat de chaque individu, contrat trié par date de survenue
# arrange() va permettre de trier les observations par identifiant et date d'entrée 
donnees_tidyverse <- donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(numero_contrat = row_number()) %>% 
  ungroup()
# À FAIRE : Dans group_by, à quoi sert le drop ?
# Numéro de l'observation : 2 manières différentes
donnees_datatable[, num_observation := .I]
donnees_datatable[, num_observation := seq_len(.N)]

# Numéro du contrat de chaque individu, contrat trié par date de survenue
setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE)
donnees_datatable[, numero_contrat := rowid(identifiant)]
donnees_datatable[, numero_contrat := seq_len(.N), by = identifiant]

10.2 Numéro de contrat par individu

proc sort data = donnees_sas;by identifiant date_entree;run;
/* L'instruction options permet de ne pas afficher d'erreur si la variable numero_contrat n'existe pas */
options dkricond=nowarn dkrocond=nowarn;
data donnees_sas;
  set donnees_sas (drop = numero_contrat);
  by identifiant date_entree;
  retain numero_contrat 0;
  if first.identifiant then numero_contrat = 1;
  else                      numero_contrat = numero_contrat + 1;
run;

options dkricond=warn dkrocond=warn;
/* Pour trier les colonnes */
data donnees_sas;
  retain identifiant date_entree numero_contrat numero_contrat;
  set donnees_sas;
run;
# 1ère ligne par identifiant
donnees_rbase[!duplicated(donnees_rbase$identifiant), , drop = FALSE]

# Dernière ligne par identifiant
donnees_rbase[!duplicated(donnees_rbase$identifiant, fromLast = TRUE), , drop = FALSE]
# 1ère ligne par identifiant
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == 1) %>% 
  ungroup()

# Autres solutions
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice(1) %>% 
  ungroup()

donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice_head(n = 1) %>% 
  ungroup()

donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == nth(row_number(), 1)) %>%
  ungroup()

# Dernière ligne par identifiant
donnees_tidyverse %>% 
group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == n()) %>% 
  ungroup()
# Autres solutions
donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  slice(n()) %>% 
  ungroup()
donnees_tidyverse %>%  
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>%
  filter(row_number() == nth(row_number(), -1)) %>%
  ungroup()
# Extraire la 1ère ligne par identifiant
donnees_datatable[, .SD[1], by = identifiant]

# Extraire la dernière ligne par identifiant
donnees_datatable[, .SD[.N], by = identifiant]

10.3 Le premier contrat, le dernier contrat, ni le premier ni le dernier contrat de chaque individu …

proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  Premier_Contrat = (first.identifiant = 1);
  Dernier_Contrat = (last.identifiant = 1);
  Ni_Prem_Ni_Der  = (first.identifiant = 0 and last.identifiant = 0);
run;
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
donnees_rbase$premier_contrat <- ifelse(!duplicated(donnees_rbase$identifiant, fromLast = FALSE), 1, 0)
donnees_rbase$dernier_contrat <- ifelse(!duplicated(donnees_rbase$identifiant, fromLast = TRUE), 1, 0)
donnees_rbase$ni_prem_ni_der  <- ifelse(! c(!duplicated(donnees_rbase$identifiant, fromLast = FALSE) | !duplicated(donnees_rbase$identifiant, fromLast = TRUE)), 1, 0)
# Premier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(premier_contrat = ifelse(row_number() == 1, 1, 0)) %>% 
  ungroup()

# Dernier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(dernier_contrat = ifelse(row_number() == n(), 1, 0)) %>% 
  ungroup()

# Ni le premier, ni le dernier contrat
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(ni_prem_ni_der = ifelse( ! (row_number() == n() | row_number() == 1), 1, 0)) %>% 
  ungroup()
donnees_datatable <- donnees_datatable[order(identifiant, date_entree, na.last = FALSE)]
donnees_datatable[, premier_contrat := fifelse(!duplicated(identifiant, fromLast = FALSE), 1, 0)]
donnees_datatable[, dernier_contrat := fifelse(!duplicated(identifiant, fromLast = TRUE), 1, 0)]
donnees_datatable[, ni_prem_ni_der := fifelse(! c(!duplicated(identifiant, fromLast = FALSE) | !duplicated(identifiant, fromLast = TRUE)), 1, 0)]

10.4 Créer une base avec les seuls premiers contrats, et une base avec les seuls derniers contrats

/* Créer une base avec les seuls premiers contrats, et une base avec les seuls derniers contrats */
proc sort data = donnees_sas;by identifiant date_entree;run;
/* Création de 2 bases en une seule étape */
data Premier_Contrat Dernier_Contrat;
  set donnees_sas;
  by identifiant date_entree;
  if first.identifiant then output Premier_Contrat;
  if last.identifiant then output Dernier_Contrat;
run;
# Créer une base avec les seuls premiers contrats, et une base avec les seuls derniers contrats
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
premier_contrat <- donnees_rbase[!duplicated(donnees_rbase$identifiant, fromLast = FALSE), ]
dernier_contrat <- donnees_rbase[!duplicated(donnees_rbase$identifiant, fromLast = TRUE), ]
ni_prem_ni_der  <- donnees_rbase[! (!duplicated(donnees_rbase$identifiant, fromLast = FALSE) | !duplicated(donnees_rbase$identifiant, fromLast = TRUE)), ]
# Créer une base avec les seuls premiers contrats, et une base avec les seuls derniers contrats

# Premier contrat
premier_contrat <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(premier_contrat = ifelse(row_number() == 1, 1, 0)) %>% 
  ungroup()

# Dernier contrat
dernier_contrat <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(dernier_contrat = ifelse(row_number() == n(), 1, 0)) %>% 
  ungroup()

# Ni le premier, ni le dernier contrat
ni_prem_ni_der <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  mutate(ni_prem_ni_der = ifelse( ! (row_number() == n() | row_number() == 1), 1, 0)) %>% 
  ungroup()
# Créer une base avec les seuls premiers contrats, et une base avec les seuls derniers contrats
donnees_datatable <- donnees_datatable[order(identifiant, date_entree, na.last = FALSE)]
premier_contrat <- donnees_datatable[!duplicated(identifiant, fromLast = FALSE), ]
dernier_contrat <- donnees_datatable[!duplicated(identifiant, fromLast = TRUE), ]
ni_prem_ni_der  <- donnees_datatable[! (!duplicated(identifiant, fromLast = FALSE) | !duplicated(identifiant, fromLast = TRUE)), ]

10.5 Sélection de lignes par identifiant

10.5.1 Les 2 premières lignes de chaque identifiant

/* Les 2 premières lignes de chaque identifiant */
proc sort data = donnees_sas;by identifiant numero_contrat;run;
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat <= 2;
quit;
# 2 premières lignes par identifiant (le premier si une seule ligne)
# Peut-on le faire en moins d'étapes ??? Avec head ?
donnees_rbase$a <- 1
donnees_rbase$numero_contrat <- ave(donnees_rbase$a, donnees_rbase$identifiant, FUN = cumsum)
deux_premieres_lignes <- donnees_rbase[which(donnees_rbase$numero_contrat <= 2), ]
donnees_rbase$a <- NULL
# REVOIR
#donnees_rbase[ave(rep(TRUE, nrow(donnees_rbase)), donnees_rbase$identifiant, FUN = function(z) seq_along(z) == 2L)]
#ind <- donnees_rbase[ave(rep(TRUE, nrow(donnees_rbase)), donnees_rbase$identifiant, FUN = function(z) length(z) == 1L | seq_along(z) == 2L)]
# Les deux premières lignes
deux_premieres_lignes <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  slice(1:2) %>% 
  ungroup()
deux_premieres_lignes <- donnees_datatable[, .SD[1:2], by = identifiant]
# Version en R Base
#https://stackoverflow.com/questions/14800161/select-the-top-n-values-by-group

10.5.2 Les 2 dernières lignes de chaque identifiant

/* Les 2 dernières lignes de chaque identifiant */
proc sort data = donnees_sas;by identifiant numero_contrat;run;
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat >= count(*) - 1;
quit;
# 2 premières lignes par identifiant (le premier si une seule ligne)
# Peut-on le faire en moins d'étapes ??? Avec head ?
donnees_rbase$a <- 1
donnees_rbase$numero_contrat <- ave(donnees_rbase$a, donnees_rbase$identifiant, FUN = cumsum)
deux_premieres_lignes <- donnees_rbase[which(donnees_rbase$numero_contrat <= 2), ]
donnees_rbase$a <- NULL
# REVOIR
#donnees_rbase[ave(rep(TRUE, nrow(donnees_rbase)), donnees_rbase$identifiant, FUN = function(z) seq_along(z) == 2L)]
#ind <- donnees_rbase[ave(rep(TRUE, nrow(donnees_rbase)), donnees_rbase$identifiant, FUN = function(z) length(z) == 1L | seq_along(z) == 2L)]
# Les deux dernières lignes
deux_dernieres_lignes <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  group_by(identifiant) %>% 
  slice(n() - 2) %>% 
  ungroup()
deux_dernieres_lignes <- donnees_datatable[, .SD[.N-2:.N], by = identifiant]
# Version en R Base
#https://stackoverflow.com/questions/14800161/select-the-top-n-values-by-group

10.5.3 2e ligne de l’individu (et rien si l’individu a 1 seule ligne)

proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sas;
  set donnees_sas;
  by identifiant date_entree;
  retain numero_contrat 0;
  if first.identifiant then numero_contrat = 1;
  else                      numero_contrat = numero_contrat + 1;
run;

/* 2 stratégies possibles */
data Deuxieme_Contrat;
  set donnees_sas;
  if numero_contrat = 2;
run;

data Deuxieme_Contrat;
  set donnees_sas (where = (numero_contrat = 2));
run;
# Avec le numéro de contrat
deuxieme_ligne <- donnees_rbase[donnees_rbase$numero_contrat == 2, ]
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
# Autre solution
donnees_rbase[unlist(tapply(seq_len(nrow(donnees_rbase)), donnees_rbase$identifiant, function(x) x[length(x)-(length(x)-1)])), ]
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  filter(row_number() == 2) %>% 
  ungroup()
deuxieme_ligne <- donnees_datatable[, .SD[2], by = identifiant]

10.5.4 L’avant-dernière ligne de l’individu (et rien si l’individu a 1 seul contrat)

/* Nécessite d'avoir le numéro du contrat */
proc sql;
  select * from donnees_sas group by identifiant
  having numero_contrat = count(*) - 1;
quit;
donnees_rbase[unlist(tapply(seq_len(nrow(donnees_rbase)), donnees_rbase$identifiant, function(x) x[length(x)-1])), ]
donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  filter(row_number() == nth(row_number(), -2))
donnees_datatable[, .SD[.N-1], by = identifiant]

10.6 Sélection par groupement

10.6.1 Personnes qui ont eu au moins une entrée en 2022

/* Personnes qui ont eu au moins une entrée en 2022 */
proc sql;
  select *
  from donnees_sas
  group by identifiant
  having sum(year(date_entree) = 2022) >= 1;
quit;
# Personnes qui ont eu au moins une entrée en 2022
auMoins2022 <- subset(donnees_rbase, identifiant %in% unique(identifiant[lubridate::year(date_entree) %in% c(2022)]))

# Autre solution : ne semble possible que pour une seule variable
auMoins2022 <- donnees_rbase[with(donnees_rbase, ave(lubridate::year(date_entree) %in% c(2022), identifiant, FUN = any)), ]
# Personnes qui ont eu au moins une entrée en 2022
auMoins2022 <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(any(lubridate::year(date_entree) == 2022))
# Personnes qui ont eu au moins une entrée en 2022
auMoins2022 <- donnees_datatable[, if(any(lubridate::year(date_entree) %in% 2022)) .SD, by = identifiant]

# Autre solution
auMoins2022 <- donnees_datatable[, if (sum(lubridate::year(date_entree) == 2022, na.rm = TRUE) > 0) .SD, by = identifiant]

10.6.2 Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée

/* Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée */
proc sql;
  create table Qualif_Non_Qualif as
  select *
  from donnees_sas
  group by identifiant
  having sum(Niveau = "Non qualifie") >= 1 and sum(Niveau = "Non qualifie") >= 1;
quit;
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée
qualif_non_qualif <- subset(
  transform(donnees_rbase, 
            qualif     = ave(niveau, identifiant, FUN = function(x) sum(ifelse(x == "Qualifié", 1, 0), na.rm = TRUE)), 
            non_qualif = ave(niveau, identifiant, FUN = function(x) sum(ifelse(x == "Non Qualifié", 1, 0), na.rm = TRUE))),
  qualif >= 1 & non_qualif >= 1)
# https://stackoverflow.com/questions/49669862/how-to-group-by-in-base-r
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée
qualif_non_qualif <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(any(niveau == "Qualifié")) %>% 
  filter(any(niveau == "Non qualifié")) %>% 
  ungroup()
# Personnes qui ont suivi à la fois une formation qualifiée et une formation non qualifiée

# Méthode la plus simple
donnees_datatable[, if (sum(niveau == "Qualifié", na.rm = TRUE) > 0 & sum(niveau == "Non qualifié", na.rm = TRUE) > 0) .SD, by = identifiant]

# Autre méthode
donnees_datatable[, `:=` (qualif = sum(fifelse(niveau == "Qualifié", 1, 0), na.rm = TRUE),
                          non_qualif = sum(fifelse(niveau == "Non qualifié", 1, 0), na.rm = TRUE)),
                by = identifiant][qualif > 0 & non_qualif > 0]

# Autre méthode
donnees_datatable[, `:=` (qualif = sum(niveau == "Qualifié", na.rm = TRUE), non_qualif = sum(niveau == "Non qualifié", na.rm = TRUE)), by = identifiant][qualif > 0 & non_qualif > 0]


# Group by et Having de SQL
# https://github.com/Rdatatable/data.table/issues/788

10.6.3 Personnes qui ont suivi deux contrats, et seulement deux, dont l’un au moins ayant débuté en 2022

/* Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins ayant débuté en 2022 */
proc sql;
  create table Deux_Contrats as
  select *
  from donnees_sas
  group by identifiant
  having count(*) = 2 and sum(year(date_entree) = 2022) >= 1;
quit;
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins ayant débuté en 2022
deux_contrats <- subset(
  transform(donnees_rbase, 
            nb = ave(identifiant, identifiant, FUN = length), 
            an = ave(date_entree, identifiant, 
                     FUN = function(x) 
                       sum(ifelse(lubridate::year(x) == 2022, 1, 0), na.rm = TRUE))),
  nb == 2 & an >= 1)
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins ayant débuté en 2022
deux_contrats <- donnees_tidyverse %>% 
  group_by(identifiant) %>%
  filter(n() == 2) %>% 
  filter(any(lubridate::year(date_entree) == 2022)) %>%
  ungroup()
# Personnes qui ont suivi deux contrats, et seulement deux, dont l'un au moins ayant débuté en 2022
donnees_datatable[, if (.N == 2 & sum(lubridate::year(date_entree) == 2022, na.rm = TRUE) >= 1) .SD, by = identifiant]

10.7 Ajouter une colonne désignant la note moyenne de Note_Contenu par individu

/* 1ère solution */
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;
  var Note_Contenu;by identifiant;output out = Temp;
run;
data Temp;
  set Temp (where = (_STAT_ = "MEAN"));
  keep identifiant Note_Contenu;
  rename Note_Contenu = Note_Contenu_Moyenne;
run;
data donnees_sas;
  merge donnees_sas (in = a) Temp (in = b);
  by identifiant;
  if a;
run;

/* 2e solution : plus souple */
/* Pour supprimer la variable ajoutée lors de la 1ère solution */
data donnees_sas;
  set donnees_sas (drop = Note_Contenu_Moyenne);
run;
proc sql;
  create table donnees_sas as
  select *
  from donnees_sas a left join
       (select identifiant, mean(Note_Contenu) as Note_Contenu_Moyenne
        from donnees_sas group by identifiant) b
       on a.identifiant = b.identifiant
  order by identifiant;
quit;
donnees_rbase <- transform(donnees_rbase, 
                           note_contenu_moyenne = ave(note_contenu, identifiant, FUN = mean, na.rm = TRUE), 
                           note_contenu_somme   = ave(note_contenu, identifiant, FUN = sum,  na.rm = TRUE))
donnees_tidyverse <- donnees_tidyverse %>%
  group_by(identifiant) %>%
  mutate(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE),
         note_contenu_somme   = sum(note_contenu, na.rm = TRUE)) %>% 
  ungroup()
donnees_datatable[, `:=` (note_contenu_moyenne = mean(note_contenu, na.rm = TRUE),
                          note_contenu_somme = sum(note_contenu, na.rm = TRUE)), by = identifiant]
# Moyenne de chaque note
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_datatable[, paste0(notes, "_m") := lapply(.SD, mean, na.rm = TRUE), .SDcols = notes, keyby = identifiant]

10.8 Variable retardée (lag)

/* La date de fin du contrat précédent (lag) */
proc sort data = donnees_sas;by identifiant date_entree;run;
data donnees_sasBon;
  set donnees_sas;
  by identifiant date_entree;  
  format Date_fin_1 ddmmyy10.;
  Date_fin_1 = lag(Date_sortie);
  if first.identifiant then Date_fin_1 = .;
run;

/* ATTENTION au lag DANS UNE CONDITION IF (cf. document) */
proc sort data = donnees_sas;by identifiant date_entree;run;
data Lag_Bon;
  set donnees_sas (keep = identifiant date_entree date_sortie);
  format date_sortie_1 lag_faux lag_bon ddmmyy10.;
  /* Erreur */
  if date_entree = lag(date_sortie) + 1 then lag_faux = lag(date_sortie) + 1;
  /* Bonne écriture */
  date_sortie_1 = lag(date_sortie);
  if date_entree = date_sortie_1 + 1 then lag_bon = date_sortie_1 + 1;
run;
# La date de fin du contrat précédent
donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entre, na.last = FALSE), ]
# Il n'existe pas de fonction lag dans le R de base (à notre connaissance)
# Il faut soit utiliser un package, soit utiliser cette astuce
donnees_rbase$date_sortie_1 <- c(as.Date(NA), donnees_rbase$date_sortie[ 1:(length(donnees_rbase$date_sortie) - 1)])
# Peut-on aussi utiliser tail(..., -1) ?

# La date du contrat futur (lead)
donnees_rbase$date_sortie__1 <- c(donnees_rbase$date_sortie[ 2:(length(donnees_rbase$date_sortie))], as.Date(NA))

# Autres solutions
#https://stackoverflow.com/questions/3558988/basic-lag-in-r-vector-dataframe
# La date de fin du contrat précédent
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(date_sortie_1 = lag(date_sortie))

# La date du contrat futur (lead)
donnees_tidyverse <- donnees_tidyverse %>% 
  # Pour trier les données de la même façon que SAS
  arrange(identifiant, !is.na(date_entree), date_entree) %>% 
  mutate(date_sortie__1 = lead(date_sortie))
# La date de fin du contrat précédent
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
donnees_datatable[, date_sortie_1 := shift(.SD, n = 1, fill = NA, "lag"), .SDcols = "date_sortie"]
donnees_datatable[, .(date_sortie, date_sortie_1)]

# La date du contrat futur (lead)
setorderv(donnees_datatable, cols = c("identifiant", "date_entree"), order = c(1L, 1L), na.last = FALSE)
donnees_datatable[, date_sortie__1 := shift(.SD, n = 1, fill = NA, "lead"), .SDcols = "date_sortie"]
donnees_datatable[, .(date_sortie, date_sortie__1)]

# Autres solutions
#https://stackoverflow.com/questions/3558988/basic-lag-in-r-vector-dataframe

10.9 Transposition de bases bases

10.9.1 Transposer une base

/* On commence déjà par calculer un tableau croisé comptant les occurrences */
proc freq data = donnees_sas;table Sexef * cspf / out = Nb;run;
proc sort data = Nb;by cspf Sexef;run;
proc print data = Nb;run;
/* On transpose le tableau */
proc transpose data = Nb out = transpose;by cspf;var count;id Sexef;run;
data transpose;set transpose (drop = _name_ _label_);run;
proc print data = transpose;run;
# On commence déjà par calculer un tableau croisé comptant les occurrences
# as.data.frame.matrix est nécessaire, car le résultat de xtabs est un array
nb <- as.data.frame.matrix(xtabs( ~ cspf + sexef, data = donnees_rbase))
# On transpose le tableau
nb_transpose <- as.data.frame(t(nb))
# On commence déjà par calculer un tableau croisé comptant les occurrences
nb <- donnees_tidyverse %>% 
  count(cspf, sexef) %>% 
  spread(sexef, n)

# On transpose le tableau
nb_transpose <- nb %>% 
  rownames_to_column() %>% 
  gather(variable, value, -rowname) %>%  
  spread(rowname, value)

# Autre solution avec les packages janitor et sjmisc
library(janitor)
nb <- donnees_tidyverse %>%
  janitor::tabyl(cspf, sexef) %>% 
  # colonne cspf comme nom de ligne
  column_to_rownames(var="cspf")

library(sjmisc)
nb_transpose <- nb %>%
  sjmisc::rotate_df()
# Etablissement d'un tableau croisé comptant les occurrences
nb <- donnees_datatable[, .N, by = list(cspf, sexef)]
# On transpose le tableau
data.table::dcast(nb, cspf ~ sexef, value.var = "N")

10.9.2 Passer d’une base en largeur (wide) à une base en longueur (long)

%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;var &notes.;output out = Temp;by identifiant;run;
data Wide;
  set Temp (where = (_STAT_ = "MEAN") drop = _TYPE_ _FREQ_);
  keep identifiant &notes.;
  drop _STAT_;
run;

/* On passe de Wide à Long */
proc transpose data = Wide out = Long;by Identifiant;var &notes.;run;
# On souhaite mettre les notes en ligne et non en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
wide_rbase <- aggregate(donnees_rbase[, varNotes], donnees_rbase[, "identifiant", drop = FALSE], mean, na.rm = TRUE)
long_rbase <- reshape(data = wide_rbase,
                varying = varNotes, 
                v.names = "notes",
                timevar = "type_note", 
                times = varNotes,
                new.row.names = NULL,
                direction = "long")
long_rbase <- long_rbase[order(long_rbase$identifiant), ]
row.names(long_rbase) <- NULL
# On souhaite mettre les notes en ligne et non en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
wide_tidyverse <- donnees_tidyverse %>% 
  group_by(identifiant) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE)))
# On l'exprime en format long
# Mise en garde : ne pas écrire value_to !
long_tidyverse <- wide_tidyverse %>% 
  pivot_longer(cols = !identifiant,
               names_to = "type_note",
               values_to = "note") %>% 
  arrange(type_note, identifiant)
# On souhaite mettre les notes en ligne et non en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
wide_datatable <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), keyby = identifiant, .SDcols = varNotes]
long_datatable <- melt(wide_datatable,
                       id.vars = c("identifiant"),
                       measure.vars = varNotes,
                       variable.name = "type_note",
                       value.name = "note")

10.9.3 Passer d’une base en longueur (long) à une base en largeur (wide)

/* On souhaite mettre les notes en ligne et non en colonne */
/* On commence par calculer les notes moyennes par identifiant */
%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc sort data = donnees_sas;by identifiant;run;
proc means data = donnees_sas mean noprint;var &notes.;output out = Temp;by identifiant;run;
data Wide;
  set Temp (where = (_STAT_ = "MEAN") drop = _TYPE_ _FREQ_);
  keep identifiant &notes.;
  drop _STAT_;
run;

/* On passe de Wide à Long */
proc transpose data = Wide out = Long;by Identifiant;var &notes.;run;
data Long;set Long (rename = (_NAME_ = Type_Note COL1 = Note));run;
/* On passe de Long à Wide */
proc transpose data = Long out = Wide;
  by Identifiant;
  var Note;
  id Type_Note;
run;
# Passer de long à wide : on souhaite revenir à la situation initiale
wide_rbase <- reshape(long_rbase, 
                timevar = "type_note",
                idvar = c("identifiant", "id"),
                direction = "wide")
#https://stats.oarc.ucla.edu/r/faq/how-can-i-reshape-my-data-in-r/
# Passer de long à wide : on souhaite revenir à la situation initiale
# Mise en garde : ne pas écrire value_from !
wide_tidyverse <- pivot_wider(long_tidyverse, 
                              names_from = type_note,
                              values_from = note)
wide_datatable <- dcast(long_datatable, identifiant ~ type_note, value.var = "note")

11 Gestion par rangées de lignes

11.1 Sélectionner les lignes avec au moins une note < 10

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data Note_Inferieure_10;
  set donnees_sas;
  %macro Inf10;
    %global temp;
    %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let j = %scan(&notes., &i.);
      &j._inf_10 = (&j. < 10 and not missing(&j.));
      %let temp = &temp. &j._inf_10;
    %end;
  %mend Inf10;
  %Inf10;
  if sum(of &temp.) >= 1;
  drop &temp.;
run;
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_rbase[apply(donnees_rbase[, varNotes], 1, function(x) any(x < 10, na.rm = TRUE)), ]
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse %>%
  filter(if_any(varNotes, ~ .x < 10))
# Autre solution
donnees_tidyverse %>%
  filter_at(varNotes, any_vars(. < 10))
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
note_moins_10 <- donnees_datatable[donnees_datatable[, .I[rowSums(.SD < 10, na.rm = TRUE) >= 1], .SDcols = varNotes]]
# Autre solution
# Le Reduce(`|`, ...) permet d'appliquer la condition | (ou) à tous les élements de la ligne, qui sont une vérification d'un nb < 10
note_moins_10 <- donnees_datatable[donnees_datatable[, Reduce(`|`, lapply(.SD, `<`, 10)), .SDcols = varNotes]]

# https://arelbundock.com/posts/datatable_rowwise/

11.2 Sélectionner les lignes avec toutes les notes supérieurs à 10

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data Note_Sup_10;
  set donnees_sas;
  %macro Inf10;
    %global temp;
    %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let j = %scan(&notes., &i.);
      &j._sup_10 = (&j. >= 10);
      %let temp = &temp. &j._sup_10;
    %end;
  %mend Inf10;
  %Inf10;
  a = sum(of &temp.);
  b = %sysfunc(countw(&notes.));
  if sum(of &temp.) = %sysfunc(countw(&notes.));
  drop &temp.;
run;
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# À FAIRE : REVOIR LE PB DES VALEURS MANQUANTES !!!!
donnees_rbase[apply(donnees_rbase[, varNotes], 1, function(x) all(x >= 10, na.rm = TRUE)), ]
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse %>%
  filter(if_all(varNotes, ~ . >= 10))
# Autre solution
donnees_tidyverse %>%
  filter_at(varNotes, all_vars(. >= 10))
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
note_sup_10 <- donnees_datatable[
  donnees_datatable[, .I[rowSums(.SD >= 10, na.rm = TRUE) == length(varNotes)], .SDcols = varNotes]]
# Autre solution
note_sup_10 <- donnees_datatable[donnees_datatable[, Reduce(`&`, lapply(.SD, `>=`, 10)), .SDcols = varNotes]]

11.3 Moyenne par ligne

/* Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  /* 1ère solution */
  Note_moyenne    = mean(of &notes.);
  /* 2e solution : l'équivalent des list-comprehension de Python en SAS */
  %macro List_comprehension;
    Note_moyenne2 = mean(of %do i = 1 %to %sysfunc(countw(&notes.));
                          %let j = %scan(&notes., &i.);
                          &j.
                         %end;);;
  %mend List_comprehension;
  %List_comprehension;
run;
/* Note moyenne (moyenne des moyennes), non pondérée et pondérée */
proc means data = donnees_sas mean;var Note_moyenne;run;
proc means data = donnees_sas mean;var Note_moyenne;weight poids_sondage;run;
# Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne
varNotes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# apply permet d'appliquer une fonctions aux lignes (1) ou colonnes (2) d'un data.frame
donnees_rbase$note_moyenne <- apply(donnees_rbase[, varNotes], 1, mean, na.rm = TRUE)
# Autre possibilité
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, varNotes], na.rm = TRUE)
# Note moyenne (moyenne des moyennes), non pondérée et pondérée
mean(donnees_rbase$note_moyenne, na.rm = TRUE)
weighted.mean(donnees_rbase$note_moyenne, donnees_rbase$poids_sondage, na.rm = TRUE)
# Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne
varNotes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# Codes à privilégier
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(pick(all_of(varNotes)), na.rm = TRUE))
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))
# Alternative très lente !
# Noter l'utilisation de c_across pour traiter automatiquement plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>% 
  rowwise() %>% 
  mutate(note_moyenne = mean(c_across(all_of(varNotes)), na.rm = TRUE)) %>% 
  ungroup()


# Note moyenne (moyenne des moyennes) non pondérée
donnees_tidyverse %>% pull(note_moyenne) %>% mean(na.rm = TRUE)
donnees_tidyverse %>% summarise(Moyenne = mean(note_moyenne, na.rm = TRUE))
# Note moyenne (moyenne des moyennes) pondérée
donnees_tidyverse %>% summarise(Moyenne_ponderee = weighted.mean(note_moyenne, poids_sondage, na.rm = TRUE))
# Pour chaque observation, 5 notes sont renseignées. On calcule la moyenne de ces 5 notes pour chaque ligne
varNotes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
# On souhaite moyenner les notes par formation
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = varNotes]
# Manière alternative, qui ne semble pas fonctionner
#donnees_datatable[, note_moyenne := Reduce(function(...) sum(..., na.rm = TRUE), .SD),
#                  .SDcols = varNotes,
#                  by = 1:nrow(donnees_datatable)]
#donnees_datatable[, do.call(function(x, y) sum(x, y, na.rm = TRUE), .SD), .SDcols = varNotes, by = 1:nrow(donnees_datatable)]

# Note moyenne (moyenne des moyennes), non pondérée et pondérée
donnees_datatable[, mean(note_moyenne, na.rm = TRUE)]
donnees_datatable[, weighted.mean(note_moyenne, poids_sondage, na.rm = TRUE)]

11.4 La note donnée est-elle supérieure à la moyenne ?

/* On crée une macro-variable SAS à partir de la valeur de la moyenne */
proc sql noprint;select mean(Note_moyenne) into :moyenne from donnees_sas;quit;
data donnees_sas;
  set donnees_sas;
  Note_Superieure_Moyenne = (Note_moyenne > &moyenne.);
run;
proc freq data = donnees_sas;tables Note_Superieure_Moyenne;run;
moyenne <- mean(donnees_rbase$note_moyenne, na.rm = TRUE)
donnees_rbase$note_superieure_moyenne <- ifelse(donnees_rbase$note_moyenne > moyenne, 1, 0)
table(donnees_rbase$note_superieure_moyenne, useNA = "always")
moyenne <- donnees_tidyverse %>% pull(note_moyenne) %>% mean(na.rm = TRUE)
donnees_tidyverse <- donnees_tidyverse %>% mutate(note_superieure_moyenne = ifelse(note_moyenne > moyenne, 1, 0))
donnees_tidyverse %>% pull(note_superieure_moyenne) %>% table(useNA = "always")
moyenne <- donnees_datatable[, mean(note_moyenne, na.rm = TRUE)]
donnees_datatable[, note_superieure_moyenne := fcase(note_moyenne >= moyenne, 1,
                                                     note_moyenne <  moyenne, 0)]
table(donnees_datatable$note_superieure_moyenne, useNA = "always")

11.5 Moyenne par ligne

/* On souhaite affecter les pondérations suivantes aux notes :
Note_Contenu : 30%, Note_Formateur : 20%, Note_Moyens : 25%, Note_Accompagnement : 15%, Note_Materiel : 10% */
/* Voici une solution possible. Une alternative intéressante serait de passer par IML (non traité ici) */
%let ponderation = 0.3 0.2 0.25 0.15 0.10;
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  %macro Somme_pond;
    %global temp;
    %let temp = ;
    %do i = 1 %to %sysfunc(countw(&notes.));
      %let k = %scan(&notes., &i.);
      %let l = %scan(&ponderation., &i., %str( ));
      &k._pond = &k. * &l.;
      %let temp = &temp. &k._pond;
    %end;
  %mend Somme_pond;
  %Somme_pond;
  Note_moyenne_pond = sum(of &temp.);
  drop &temp.;
run;
proc means data = donnees_sas mean;var Note_moyenne_pond;run;
# On calcule de nouveau cette moyenne, mais en pondérant
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
sum(ponderation)
donnees_rbase$note_moyennepond <- apply(donnees_rbase[, notes], 1, function(x) weighted.mean(x, ponderation, na.rm = TRUE))
# Autre manière, en exploitant le calcul matriciel
# Ne fonctionne pas, du fait des NA
as.matrix(donnees_rbase[, notes]) %*% as.matrix(ponderation)
# Produit élément par élément
# On peut procéder par produit matriciel
as.matrix(donnees_rbase[, notes]) * matrix(t(as.matrix(ponderation)), nrow(donnees_rbase), 5)
# On calcule de nouveau cette moyenne, mais en pondérant
# On souhaite affecter les pondérations suivantes aux notes :
# note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
# La fonction RowMeans ne fonctionne plus, cette fois !
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
sum(ponderation)
# Noter l'utilisation de c_across pour traiter automatiquement plusieurs variables
donnees_tidyverse <- donnees_tidyverse %>%
  rowwise() %>%
  mutate(note_moyenne = weighted.mean(c_across(varNotes), ponderation, na.rm = TRUE)) %>% 
  ungroup()
## On souhaite affecter les pondérations suivantes aux notes :
## note_contenu : 30%, note_formateur : 20%, note_moyens : 25%, note_accompagnement : 15%, note_materiel : 10%
notes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
ponderation <- c(note_contenu = 30, note_formateur = 20, note_moyens = 25, note_accompagnement = 15, note_materiel = 10) / 100
donnees_datatable[, note_moyenne_pond := rowSums(mapply(FUN = `*`, .SD, ponderation), na.rm = TRUE), .SDcols = names(ponderation)]

12 Les valeurs manquantes

12.1 Repérer les valeurs manquantes (variables Âge et Niveau)

data Missing;
  set donnees_sas;
  /* 1ère solution */
  if missing(age) or missing(Niveau) then missing1 = 1;else missing1 = 0;
  /* 2e solution */
  if age = . or Niveau = '' then missing2 = 1;else missing2 = 0;
  keep Age Niveau Missing1 Missing2;
run;
donnees_rbase$manquant <- ifelse(is.na(donnees_rbase$age) | is.na(donnees_rbase$niveau), 1, 0)
# Mauvaise méthode pour repérer les valeurs manquantes
ageManquant <- donnees_rbase[donnees_rbase$age == NA,  ]
# Bonne méthode pour repérer les valeurs manquantes
ageManquant <- donnees_rbase[is.na(donnees_rbase$age), ]
donnees_tidyverse <- donnees_tidyverse %>% 
  mutate(manquant = ifelse(is.na(age) | is.na(niveau), 1, 0))

# Mauvaise méthode pour repérer les valeurs manquantes
ageManquant <- donnees_tidyverse %>%
  filter(age == NA)
# Bonne méthode pour repérer les valeurs manquantes
ageManquant <- donnees_tidyverse %>%
  filter(is.na(age))
donnees_datatable[, manquant := fifelse(is.na(donnees_datatable$age) | is.na(donnees_datatable$niveau), 1, 0)]
ageManquant <- donnees_datatable[age == NA] # Faux
ageManquant <- donnees_datatable[is.na(age)] # Correct

12.2 Nombre et proportion de valeurs manquantes par variable

/* Pour les variables numériques ou date */
/* Partie "Missing Values" en bas du tableau consacré à la variable */
proc univariate data = donnees_sas;var _numeric_;run;

/* Pour l'ensemble des variables */
/* Une solution possible */
%macro Iteration(base = donnees_sas);
  %local nbVar;
  proc contents data = donnees_sas out = ListeVar noprint;run;
  proc sql noprint;select count(*) into :nbVar from ListeVar;quit;
  %do i = 1 %to &nbVar.;
    data _null_;
      set ListeVar (firstobs = &i. obs = &i.);
      call symput('var', name);
    run;
    proc sql;
      select max("&var.") as Variable, sum(missing(&var.)) as Manquants, sum(missing(&var.)) / count(*) * 100 as Prop_Manquants
      from &base.;
    quit;
  %end;
  proc datasets lib = work nolist;delete ListeVar;run;
%mend Iteration;
%Iteration;
# Pour les variables numériques ou date
apply(is.na(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))]
  ), 2, mean) * 100
# Autres solutions
sapply(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))],
  function(x) mean(is.na(x)) * 100)
sapply(
  donnees_rbase[sapply(donnees_rbase, function(x) is.numeric(x) | lubridate::is.Date(x))],
  function(x) sum(is.na(x)) / length(x) * 100)

# Pour l'ensemble des variables
colMeans(is.na(donnees_rbase)) * 100
apply(is.na(donnees_rbase), 2, mean) * 100
# Pour les variables numériques ou date
donnees_tidyverse %>%
  summarise(across(where(~ is.numeric(.x) | lubridate::is.Date(.x)),
                   list(~sum(is.na(.x)), ~mean(is.na(.x)))))
donnees_tidyverse %>%
  summarise(across(where(~ is.numeric(.x) | lubridate::is.Date(.x)),
                   list(~sum(is.na(.x)), ~sum(is.na(.x)) / length(.x))))

# Pour l'ensemble des variables
donnees_tidyverse %>%
  summarise(across(everything(), ~mean(is.na(.x))))
# Autres solutions
donnees_tidyverse %>% map(~ mean(is.na(.)) * 100)
# Obsolète
donnees_tidyverse %>% summarise_each(funs(mean(is.na(.)) * 100))
# Pour les variables numériques ou date
donnees_datatable[, lapply(.SD, function(x) mean(is.na(x)) * 100), .SDcols = function(x) c(lubridate::is.Date(x) | is.numeric(x))]
# Pour l'ensemble des variables
donnees_datatable[, lapply(.SD, function(x) mean(is.na(x)) * 100)]

12.3 Incidence des valeurs manquantes

/* En SAS, les valeurs manquantes sont des nombres négatifs faibles */
data Valeur_Manquante;
  set donnees_sas;
  /* Lorsque Age est manquant (missing), Jeune_Correct vaut 0 mais Jeune_Incorrect vaut 1 */
  /* En effet, pour SAS, un Age manquant est une valeur inférieure à 0, donc bien inférieure à 25.
     Donc la variable Jeune_Incorrect vaut bien 1 pour les âges inconnus */
  Jeune_Incorrect = (Age <= 25);
  Jeune_Correct   = (0 <= Age <= 25);
run;
proc print data = Valeur_Manquante (keep  = Age Jeune_Correct Jeune_Incorrect
                                    where = (missing(Age)));
run;
proc freq data = Valeur_Manquante;tables Jeune_Incorrect Jeune_Correct;run;
mean(donnees_rbase$note_formateur)
mean(donnees_rbase$note_formateur, na.rm = TRUE)
donnees_tidyverse %>% pull(note_formateur) %>% mean()
donnees_tidyverse %>% pull(note_formateur) %>% mean(na.rm = TRUE)
# Attention, en tidyverse, les syntaxes suivantes ne fonctionnent pas !
# NE PAS ECRIRE !
# donnees_tidyverse %>% mean(note_formateur)
# donnees_tidyverse %>% mean(note_formateur, na.rm = TRUE)
donnees_datatable[, mean(note_formateur)]
donnees_datatable[, mean(note_formateur, na.rm = TRUE)]

12.4 Remplacer toutes les valeurs numériques manquantes par 0

/* On sélectionne toutes les variables numériques */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :nom_col separated by " " from Var where format = "";
run;
data donnees_sas_sans_missing;
  set donnees_sas;
  %macro Missing;
    %local i var;
    %do i = 1 %to %sysfunc(countw(&nom_col.));
      %let var = %scan(&nom_col., &i);
      if missing(&var.) then &var. = 0;
    %end;
  %mend Missing;
  %Missing;
run;
proc datasets lib = Work nolist;delete Var;run;
# Dans le cas des dates, la valeur manquante a été remplacée par 1970-01-01
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na[is.na(donnees_rbase_sans_na)] <- 0
# On remplace seulement les valeurs numériques par 0
donnees_rbase_sans_na <- donnees_rbase
varNumeriques <- names(donnees_rbase)[unlist(lapply(donnees_rbase, is.numeric))]
donnees_rbase_sans_na[, varNumeriques][is.na(donnees_rbase_sans_na[, varNumeriques])] <- 0
# Autre solution, avec replace
donnees_rbase_sans_na[, varNumeriques] <- lapply(donnees_rbase_sans_na[, varNumeriques],
                                                 function(x) {replace(x, is.na(x), 0)})
# On remplace seulement les valeurs numériques par 0
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), ~tidyr::replace_na(.x, 0)))
# Autres façons d'écrire les fonctions anonymes
# La méthode complète
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), function(x) tidyr::replace_na(x, 0)))
# Une autre façon de raccourcir (depuis R 4.1)
# \(x) est un raccourci pour function(x)
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(where(is.numeric), \(x) tidyr::replace_na(x, 0)))
# Autre solution
donnees_tidyverse_sans_na <- donnees_tidyverse %>%
  purrr::modify_if(is.numeric, ~tidyr::replace_na(.x, 0))
donnees_datatable_sans_na <- copy(donnees_datatable)
setnafill(donnees_datatable[, .SD, .SDcols = is.numeric], fill = 0)
# Autre solution
donnees_datatable_sans_na <- copy(donnees_datatable)
cols <- colnames(donnees_datatable_sans_na[, .SD, .SDcols = is.numeric])
donnees_datatable_sans_na[, (cols) := lapply(.SD, function(x) fifelse(is.na(x), 0, x)), .SDcols = cols]
# Ensemble des colonnes
donnees_datatable_sans_na <- copy(donnees_datatable)
donnees_datatable_sans_na[is.na(donnees_datatable_sans_na)] <- 0

12.5 Remplacer les valeurs manquantes d’une seule variable par 0

%let var = note_contenu;
data donnees_sas_sans_missing;
  set donnees_sas;
  if missing(&var.) then &var. = 0;
  /* Ou alors */
  if &var. = . then &var. = 0;
  /* Ou encore */
  if note_contenu = . then note_contenu = 0;
run;
variable <- "note_contenu"
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na[, variable][is.na(donnees_rbase_sans_na[, variable])] <- 0
donnees_rbase_sans_na[, variable] <- replace(donnees_rbase_sans_na[, variable],
                                             is.na(donnees_rbase_sans_na[, variable]), 0)
# Ou alors
donnees_rbase_sans_na <- donnees_rbase
donnees_rbase_sans_na$note_contenu[is.na(donnees_rbase_sans_na$note_contenu)] <- 0
variable <- "note_contenu"
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(across(variable,  ~tidyr::replace_na(.x, 0)))
# Ou alors
donnees_tidyverse_sans_na <- donnees_tidyverse %>% 
  mutate(note_contenu = tidyr::replace_na(note_contenu, 0))
variable <- "note_contenu"
donnees_datatable[, replace(.SD, is.na(.SD), 0), .SDcols = variable]
donnees_datatable[, lapply(.SD, function(x) fifelse(is.na(x), 0, x)), .SDcols = variable]
donnees_datatable[, lapply(.SD, \(x) fifelse(is.na(x), 0, x)), .SDcols = variable]
# Ou alors
donnees_datatable[, replace(.SD, is.na(.SD), 0), .SDcols = "note_contenu"]

13 Les doublons

13.1 Doublons pour toutes les colonnes

/* On extraie tous les doublons, pas la première occurrence */

/* On récupère déjà la dernière variable de la base (on en aura besoin plus loin) */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :derniere_var
  from Var
  where varnum = (select max(varnum) from Var);
quit;
proc sort data = donnees_sas;by &nom_col.;run;
data Doublons;
  set donnees_sas;
  by &nom_col.;
  if not (first.&derniere_var. and last.&derniere_var.);
run;
# On extraie tous les doublons, pas la première occurrence
doublons <- donnees_rbase[duplicated(donnees_rbase), ]
# On extraie tous les doublons, pas la première occurrence
doublons <- donnees_tidyverse %>%  
  group_by_all() %>% 
  filter(n() > 1) %>%
  slice(-1) %>%
  ungroup()
# On extraie tous les doublons, pas la première occurrence
doublons <- donnees_datatable[duplicated(donnees_datatable), ]

13.2 Doublons pour une ou plusieurs colonnes

/* On extraie tous les doublons, pas la première occurrence */
%let var = identifiant;
proc sort data = donnees_sas;by &var.;run;
data doublons;
  set donnees_sas;
  by &var.;
  if not first.&var.;
run;
# On extraie tous les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_rbase[duplicated(donnees_rbase[, variable]), ]
# On extraie tous les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() > 1) %>%
  slice(-1) %>%
  ungroup()
# On extraie tous les doublons, pas la première occurrence
variable <- "identifiant"
doublons <- donnees_datatable[duplicated(donnees_datatable[, ..variable]), ]

13.3 Récupérer toutes les lignes pour les identifiants en doublon

%let var = identifiant;
/* On groupe par la colonne identifiant, et si on aboutit à strictement plus d'une ligne, c'est un doublon */
proc sql;
  create table enDouble as
  select * from donnees_sas
  group by &var.
  having count(*) > 1;
quit;
variable <- "identifiant"
enDouble <- donnees_rbase[donnees_rbase[, variable] %in%
                            donnees_rbase[duplicated(donnees_rbase[, variable]), variable]]
variable <- "identifiant"
enDouble <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() > 1) %>%
  ungroup()
variable <- "identifiant"
enDouble <- donnees_datatable[donnees_datatable[[variable]] %chin%
                                donnees_datatable[[variable]][duplicated(donnees_datatable[[variable]])], ]

13.4 Récupérer toutes les lignes pour les identifiants sans doublon

%let var = identifiant;
proc sql;
  create table sansDouble as
  select * from donnees_sas
  group by &var.
  having count(*) = 1;
quit;
variable <- "identifiant"
sansDouble <- donnees_rbase[! donnees_rbase[, variable] %in%
                              donnees_rbase[duplicated(donnees_rbase[, variable]), variable]]
variable <- "identifiant"
sansDouble <- donnees_tidyverse %>%  
  group_by(across(variable)) %>% 
  filter(n() == 1) %>%
  ungroup()
variable <- "identifiant"
sansDouble <- donnees_datatable[! donnees_datatable[[variable]] %chin%
                                  donnees_datatable[[variable]][duplicated(donnees_datatable[[variable]])], ]

13.5 Suppression des doublons pour l’ensemble des variables

/* 1ère méthode */
proc sort data = donnees_sas nodupkey;
  by _all_;
run;
/* 2e méthode, avec first. et last. (cf. infra) */
/* On récupère déjà la dernière variable de la base (on en aura besoin plus loin) */
proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;
  select name into :derniere_var from Var
  where varnum = (select max(varnum) from Var);
quit;
proc sql noprint;
  select name into :nom_col separated by " " from Var order by varnum;
quit;
%put Dernière variable de la base : &derniere_var.;
proc sort data = donnees_sas;by &nom_col.;run;
data sansDouble;
  set donnees_sas;
  by &nom_col.;
  if first.&derniere_var.;
run;
donnees_rbase_sansdoublon <- donnees_rbase[! duplicated(donnees_rbase), ]
# Autre solution (équivalente à la solution first. de SAS)
donnees_rbase_sansdoublon <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE), ]
donnees_rbase_sansdoublon <- donnees_rbase[!duplicated(donnees_rbase[, colnames(donnees_rbase)], fromLast = TRUE), ]
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(pick(everything())) %>% 
  distinct()
donnees_tidyverse <- donnees_tidyverse %>% 
  arrange(across(everything())) %>% 
  distinct()
donnees_datatable <- unique(donnees_datatable)
donnees_datatable <- donnees_datatable[! duplicated(donnees_datatable), ]

13.6 Suppression des doublons pour une seule variable

proc sort data = donnees_sas;by _all_;run;
data sansDouble;
  set donnees_sas;
  by _all_;
  if first.identifiant;
run;
donnees_rbase_sansdoublon <- donnees_rbase[order(colnames(donnees_rbase), na.last = FALSE), ]
donnees_rbase_sansdoublon <- donnees_rbase_sansdoublon[!duplicated(donnees_rbase_sansdoublon$identifiant), , drop = FALSE]
# L'option .keep_all = TRUE est nécessaire 
# À FAIRE : REVOIR LE TRI PAR RAPPORT A SAS !!!
sansDouble <- donnees_tidyverse %>% 
  arrange(pick(everything())) %>% 
  distinct(identifiant, .keep_all = TRUE)
sansDouble <- donnees_tidyverse %>% 
  arrange(across(everything())) %>% 
  distinct(identifiant, .keep_all = TRUE)
setorderv(donnees_datatable, cols = colnames(donnees_datatable), na.last = FALSE)
sansDouble <- donnees_datatable[! duplicated(donnees_datatable[, c("identifiant")]), ]

13.7 Identifiants uniques

proc sql;
  create table id as select distinct identifiant from donnees_sas order by identifiant;
quit;
unique(donnees_rbase["identifiant"])
donnees_tidyverse %>%
  distinct(identifiant)
unique(donnees_datatable[, "identifiant"])

13.8 Nombre de lignes uniques, sans doublon

proc contents data = donnees_sas out = Var noprint;run;
proc sql noprint;select name into :nom_col separated by ", " from Var order by varnum;quit;
proc sql;
  select count(*) as Nb_Lignes_Uniques
  from (select &nom_col., count(*) from donnees_sas group by &nom_col.);
quit;
nrow(unique(donnees_rbase))
donnees_tidyverse %>%
  distinct() %>% 
  nrow()
# À FAIRE : pas sûr de moi
uniqueN(donnees_datatable)

14 Les jointures de bases

Pour fonctionner, les codes de cette partie nécessitent l’import des bases de la section “Importation de bases pour la jointure”.

14.1 Importation de bases pour les jointures

/* On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes */
data Diplome;
  infile cards dsd dlm='|';
  format Identifiant $3. Diplome $20.;
  input Identifiant $ Diplome $;
  cards;
  173|Bac
  168|Bep-Cap
  112|Bep-Cap
  087|Bac+2
  689|Bac+2
  765|Pas de diplôme
  113|Bac
  999|Bac
  554|Bep-Cap
  ;
run;
/* On suppose que l'on dispose aussi d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller */
data Entrevue;
  infile cards dsd dlm='|';
  format Identifiant $3. Date_entrevue ddmmyy10.;
  input Identifiant $ Date_entrevue ddmmyy10.;
  cards;
  173|06/08/2021
  168|17/10/2019
  087|12/06/2021
  689|28/03/2018
  099|01/09/2022
  765|01/10/2020
  ;
run;
/* On récupère un extrait de la base initiale */
data Jointure;
  set donnees_sas (keep = Identifiant Sexe date_entree date_sortie);
run;
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_rbase <- data.frame(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                      diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))
# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_rbase <- data.frame(identifiant = c("173", "168", "087", "689", "099", "765"),
                       date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_rbase$date_entrevue <- lubridate::dmy(entrevue_rbase$date_entrevue)
# On récupère un extrait de la base initiale
jointure_rbase <- donnees_rbase[, c("identifiant", "sexe", "date_entree", "date_sortie")]
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_tidyverse <-tibble(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                      diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))
# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_tidyverse <- tibble(identifiant = c("173", "168", "087", "689", "099", "765"),
                       date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_tidyverse <- entrevue_tidyverse %>% 
  mutate(date_entrevue = lubridate::dmy(date_entrevue))
# On récupère un extrait de la base initiale
variable <- c("identifiant", "sexe", "date_entree", "date_sortie")
jointure_tidyverse <- donnees_tidyverse %>%
  select(variable)
# On suppose que l'on dispose d'une base supplémentaire avec les diplômes des personnes
diplome_datatable <- data.table(identifiant = c("173", "168", "112", "087", "689", "765", "113", "999", "554"),
                                diplome = c("Bac", "Bep-Cap", "Bep-Cap", "Bac+2", "Bac+2", "Pas de diplôme", "Bac", "Bac", "Bep-Cap"))
# On suppose que l'on dispose d'une base supplémentaire indiquant la date d'une entrevue avec un conseiller
entrevue_datatable <- data.table(identifiant = c("173", "168", "087", "689", "099", "765"),
                                 date_entrevue = c("06/08/2021", "17/10/2019", "12/06/2021", "28/03/2018", "01/09/2022", "01/10/2020"))
entrevue_datatable[, date_entrevue := lubridate::dmy(date_entrevue)]
# On récupère un extrait de la base initiale
jointure_datatable <- donnees_datatable[, c("identifiant", "sexe", "date_entree", "date_sortie")]

14.2 Inner join : les seuls identifiants communs aux deux bases

/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Inner_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a and b;
run;
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Inner_Join2 as
  select * from Jointure a inner join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;
proc print data = Inner_Join1 (obs = 10);run;
proc sql;select count(*) from Inner_Join1;quit;
proc sql;select count(*) from Inner_Join2;quit;
# Sont appariés les identifiants communs aux deux bases
innerJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant")
dim(innerJoin)
# Sont appariés les identifiants communs aux deux bases
innerJoin <- jointure_tidyverse %>% 
  inner_join(diplome_tidyverse, by = "identifiant")
dim(innerJoin)
# Autres solutions
innerJoin <- jointure_tidyverse %>% 
  inner_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(innerJoin)
innerJoin <- inner_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(innerJoin)
innerJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant")
innerJoin <- jointure_datatable[diplome_datatable, nomatch = 0, on = list(identifiant == identifiant)]
innerJoin <- jointure_datatable[diplome_datatable, nomatch = 0, on = .(identifiant == identifiant)]
dim(innerJoin)

14.3 Left join : les identifiants de la base de gauche

/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Left_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a;
run;
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Left_Join2 as
  select * from Jointure a left join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;
proc print data = Left_Join1 (obs = 10);run;
proc sql;select count(*) from Left_Join1;quit;
proc sql;select count(*) from Left_Join2;quit;
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
leftJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all.x = TRUE)
dim(leftJoin)
# Sont appariés tous les identifiants de la base de gauche, et les correspondants éventuels de la base de droite
leftJoin <- jointure_tidyverse %>% 
  left_join(diplome_tidyverse, by = "identifiant")
dim(leftJoin)
# Autres solutions
leftJoin <- jointure_tidyverse %>% 
  left_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(leftJoin)
leftJoin <- left_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(leftJoin)
leftJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all.x = TRUE)
dim(leftJoin)
leftJoin <- diplome_datatable[jointure_datatable, on = .(identifiant == identifiant)]
dim(leftJoin)

14.4 Right join : les identifiants de la base de droite

/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Right_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if b;
run;
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Right_Join2 as
  select * from Jointure a right join Diplome b on a.identifiant = b.identifiant
  order by a.identifiant;
quit;
proc print data = Right_Join1 (obs = 10);run;
proc sql;select count(*) from Right_Join1;quit;
proc sql;select count(*) from Right_Join2;quit;
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
rightJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all.y = TRUE)
dim(rightJoin)
# Sont appariés tous les identifiants de la base de droite et les correspondants éventuels de la base de gauche
rightJoin <- jointure_tidyverse %>% 
  right_join(diplome_tidyverse, by = "identifiant")
dim(rightJoin)
# Autre solution
rightJoin <- jointure_tidyverse %>% 
  right_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(rightJoin)
rightJoin <- right_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(rightJoin)
rightJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all.y = TRUE)
dim(rightJoin)
rightJoin <- jointure_datatable[diplome_datatable, on = .(identifiant == identifiant)]
dim(rightJoin)

14.5 Full join : les identifiants des deux bases

/* Le tri préalable des bases de données à joindre par la variable de jointure est nécessaire avec la stratégie merge */
proc sort data = Diplome;by identifiant;run;
proc sort data = Jointure;by identifiant;run;
data Full_Join1;
  merge Jointure (in = a) Diplome (in = b);
  by identifiant;
  if a or b;
run;
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Full_Join2 as
  select coalesce(a.identifiant, b.identifiant) as Identifiant, *
  from Jointure a full outer join Diplome b on a.identifiant = b.identifiant
  order by calculated identifiant;
quit;
proc print data = Full_Join1 (obs = 10);run;
proc sql;select count(*) from Full_Join1;quit;
proc sql;select count(*) from Full_Join2;quit;
# Sont appariés les identifiants des deux bases
fullJoin <- merge(jointure_rbase, diplome_rbase, by.x = "identifiant", by.y = "identifiant", all = TRUE)
dim(fullJoin)
# Sont appariés les identifiants des deux bases
fullJoin <- jointure_tidyverse %>% 
  full_join(diplome_tidyverse, by = "identifiant")
dim(fullJoin)
# Autre solution
fullJoin <- jointure_tidyverse %>% 
  full_join(diplome_tidyverse, by = join_by(identifiant == identifiant))
dim(fullJoin)
fullJoin <- full_join(jointure_tidyverse, diplome_tidyverse, by = "identifiant")
dim(fullJoin)
fullJoin <- merge(jointure_datatable, diplome_datatable, by.x = "identifiant", by.y = "identifiant", all = TRUE)
dim(fullJoin)

14.6 Jointure de 3 bases ou plus en une seule opération (inner join)

proc sort data = Jointure;by identifiant;run;
proc sort data = Diplome;by identifiant;run;
proc sort data = Entrevue;by identifiant;run;
data Inner_Join3;
  merge Jointure (in = a) Diplome (in = b) Entrevue (in = c);
  by identifiant;
  if a and b and c;
run;
/* Le tri préalable des bases de données à joindre n'est pas nécessaire avec la jointure SQL */
proc sql;
  create table Inner_Join4 as
  select * from Jointure a inner join Diplome b on a.identifiant = b.identifiant
                           inner join Entrevue c on a.identifiant = c.identifiant
  order by a.identifiant;
quit;
proc print data = Inner_Join4 (obs = 10);run;
proc sql;select count(*) from Inner_Join3;quit;
proc sql;select count(*) from Inner_Join4;quit;
# Via un inner join
# Utilisation de la fonction Reduce
# Elle applique successivement (et non simultanément, comme do.call) à tous les éléments d'une liste une fonction
innerJoin2 <- Reduce(function(x, y) merge(x, y, all = FALSE, by.x = "identifiant", by.y = "identifiant"),
                     list(jointure_rbase, diplome_rbase, entrevue_rbase))
dim(innerJoin2)
# Via un inner join
# Utilisation de la fonction reduce de purrr
# Elle applique successivement (et non simultanément, comme do.call) à tous les éléments d'une liste une fonction
innerJoin2 <- list(jointure_tidyverse, diplome_tidyverse, entrevue_tidyverse) %>%
  purrr::reduce(dplyr::inner_join, by = join_by(identifiant == identifiant))
dim(innerJoin2)
# Utilisation de la fonction Reduce : elle applique successivement (et non simultanément, comme do.call) à tous les éléments d'une liste une fonction
innerJoin2 <- Reduce(function(x, y) merge(x, y, all = FALSE, by.x = "identifiant", by.y = "identifiant"),
                    list(jointure_datatable, diplome_datatable, entrevue_datatable))
dim(innerJoin2)

14.7 Jointure sur inégalités

/* On associe l'entrevue au contrat au cours duquel elle a eu lieu */
proc sql;
  create table Inner_Join_Inegalite as
  select *
  from Jointure a inner join Entrevue b
       on a.identifiant = b.identifiant and a.date_entree <= b.date_entrevue <= a.date_sortie
  order by a.identifiant;
quit;
proc print data = Inner_Join_Inegalite (obs = 10);run;
proc sql;select count(*) from Inner_Join_Inegalite;quit;
# Ne semble pas natif en R-Base.
# Une proposition indicative où on applique la sélection après la jointure, ce qui ne doit pas être très efficace ...
innerJoinInegalite <- merge(jointure_rbase, entrevue_rbase, by = "identifiant")
innerJoinInegalite <- with(innerJoinInegalite,
                           innerJoinInegalite[which(date_entree <= date_entrevue & date_entrevue <= date_sortie), ])
dim(innerJoinInegalite)
# Ne semble pas natif en R-Base.
# Une proposition indicative où on applique la sélection après la jointure, ce qui ne doit pas être très efficace ...
innerJoinInegalite <- jointure_tidyverse %>% 
  inner_join(entrevue_tidyverse, join_by(identifiant == identifiant,
                                         date_entree <= date_entrevue,
                                         date_sortie >= date_entrevue))
dim(innerJoinInegalite)
# Attention, l'ordre des conditions doit correspondre à l'ordre des bases dans la jointure !
# Il semble que l'on soit forcé de spécifier tous les noms des colonnes, et ce qui est un peu problématique ...
# À FAIRE : Peut-on faire plus simplement ??
innerJoinInegalite <- jointure_datatable[entrevue_datatable,
                                         .(identifiant, sexe, date_entree, date_sortie, date_entrevue),
                                         on = .(identifiant, date_entree <= date_entrevue, date_sortie >= date_entrevue),
                                         nomatch = 0L
                                         ][order(identifiant)]
dim(innerJoinInegalite)

14.8 Cross join : toutes les combinaisons possibles de CSP, sexe et Diplome

proc sql;
  create table CrossJoin as
  select *
  from (select distinct CSPF from donnees_sas)  cross join
       (select distinct Sexef from donnees_sas) cross join
       (select distinct Diplome from Diplome)
  order by CSPF, Sexef, Diplome;
quit;
proc sql;select count(*) from CrossJoin;quit;
# Toutes les combinaisons possibles de CSP, sexe et diplome
crossJoin <- unique(expand.grid(donnees_rbase$cspf, donnees_rbase$sexef, diplome_rbase$diplome))
colnames(crossJoin) <- c("cspf", "sexef", "diplome")
dim(crossJoin)
# Autre solution
crossJoin2 <- unique(merge(donnees_rbase[, c("cspf", "sexef")], diplome_rbase[, "diplome"], by = NULL))
dim(crossJoin2)
# https://stackoverflow.com/questions/10600060/how-to-do-cross-join-in-r
# Toutes les combinaisons possibles de CSP, sexe et diplome
crossJoin <- donnees_tidyverse %>%
  select(cspf, sexef) %>% 
  cross_join(diplome_tidyverse %>% select(diplome)) %>% 
  distinct()
dim(crossJoin)
# Autre solution
crossJoin <- cross_join(donnees_tidyverse %>% select(cspf, sexef), diplome_tidyverse %>% select(diplome)) %>% 
  distinct()
dim(crossJoin)
# Autre solution
crossJoin <- donnees_tidyverse %>% 
  tidyr::expand(cspf, sexef, diplome_tidyverse$diplome) %>%
  distinct()
dim(crossJoin)
crossJoin <- data.table::CJ(donnees_datatable$cspf, donnees_datatable$sexef, diplome_datatable$diplome, unique = TRUE)
colnames(crossJoin) <- c("cspf", "sexef", "diplome")
dim(crossJoin)

14.9 Juxtaposer côte à côte deux bases de données

/* On va ajouter le numéro de la ligne */
proc sql noprint;select count(*) into :tot from donnees_sas;run;
data Ajout;do Num_ligne = 1 to &tot.;output;end;run;
/* Le merge sans by va juxtaposer côte à côte les bases */
data Concatener;merge Ajout donnees_sas;run;
/* Si l'une des bases comprend plus de ligne que l'autre, ajout d'une ligne de valeurs manquantes */
proc sql noprint;select count(*) + 1 into :tot from donnees_sas;run;
data Ajout;do Num_ligne = 1 to &tot.;output;end;run;
data Concatener;merge Ajout donnees_sas;run;
# On va ajouter le numéro de la ligne
# cbind si les deux bases comprennent le même nombre de lignes
ajout <- data.frame(num_ligne = seq_len(nrow(donnees_rbase)))
concatener <- cbind(ajout, donnees_rbase)
# Erreur si l'une des bases comprend plus de lignes que l'autre
ajout <- data.frame(num_ligne = seq_len(nrow(donnees_rbase) + 1))
# donnees_rbase_ajout <- cbind(ajout, donnees_rbase)
# Proposition de solution
cbind_alt <- function(liste) {
  # Nombre maximal de colonnes dans la liste de dataframes
  maxCol <- max(unlist(lapply(liste, nrow)))
  # Ajout d'une colonne de valeurs manquantes pour toutes les bases ayant moins de ligne que le maximum
  res <- lapply(liste, function(x) {
    for (i in seq_len(maxCol - nrow(x))) {
      x[nrow(x) + i, ] <- NA
    }
    return(x)
  })
  # On joint les résultats
  return(do.call(cbind, res))
}
concatener <- cbind_alt(list(ajout, donnees_rbase))
# On va ajouter le numéro de la ligne
# cbind si les deux bases comprennent le même nombre de lignes
ajout <- tibble(num_ligne = seq_len(nrow(donnees_tidyverse)))
concatener <- donnees_tidyverse %>% bind_cols(ajout)
# Ne fonctionne si l'une des bases comprend plus de lignes que l'autre !
ajout <- tibble(num_ligne = seq_len(nrow(donnees_tidyverse) + 1))
#concatener <- donnees_tidyverse %>% bind_cols(ajout)
# cf. solution proposée dans R-Base
cbind_alt <- function(liste) {
  # Nombre maximal de colonnes dans la liste de dataframes
  maxCol <- max(unlist(lapply(liste, nrow)))
  # Ajout d'une colonne de valeurs manquantes pour toutes les bases ayant moins de ligne que le maximum
  res <- lapply(liste, function(x) {
    for (i in seq_len(maxCol - nrow(x))) {
      x[nrow(x) + i, ] <- NA
    }
    return(x)
  })
  # On joint les résultats
  return(bind_cols(res))
}
concatener <- cbind_alt(list(ajout, donnees_tidyverse))
# On va ajouter le numéro de la ligne
# data.frame::cbind si les deux bases comprennent le même nombre de lignes
ajout <- data.table(num_ligne = seq_len(nrow(donnees_datatable)))
concatener <- cbind(ajout, donnees_datatable)
# Fonctionne aussi avec des bases comportement un nombre différent de lignes
# Mais attention, le résultat n'est pas le même que sur SAS, il y a recycling
ajout <- data.table(num_ligne = seq_len(nrow(donnees_datatable) + 1))
concatener <- cbind(ajout, donnees_datatable)

14.10 Empiler deux bases de données

/* On va empiler la somme des notes en dessous de la base des notes */
%let var = Identifiant Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
/* On sélectionne un nombre réduit de variables pour simplifier l'exemple */
%let var2 = %sysfunc(tranwrd(&var., Identifiant,));
data Notes;set donnees_sas (keep = &var.);run;
/* Moyenne des notes par individu */
proc means data = Notes noprint mean;var &var2.;output out = Ajout mean = &var2.;run;
/* On concatène avec les données. Valeur manquante si les variables ne correspondent pas */
/* L'instruction set permet de concaténer les bases */
data Empiler;set Notes Ajout (drop = _type_ _freq_);run;
/* Autre solution, proc append */
data Empiler;set Notes;run;
proc append base = Empiler data = Ajout force;run;
/* On renomme la ligne des moyennes ajoutée */
data Empiler;
  set Empiler nobs = nobs;
  if _N_ = nobs then Identifiant = "Moyenne";
run;
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes par individu
moyennes <- data.frame(t(colMeans(donnees_rbase[, varNotes], na.rm = TRUE)))
# On sélectionne la base des notes
notes <- donnees_rbase[, varNotes]
# rbind lorsque les bases empilées ont le même nombre de colonne
empiler <- rbind(notes, moyennes)
# Mais, ne fonctionne plus si l'on concatène des bases de taille différente
notes <- donnees_rbase[, c("identifiant", varNotes)]
# Ne fonctionne pas
#empiler <- rbind(notes, moyennes)
# Une solution alternative, lorsque le nombre de colonnes diffère entre les deux bases
# Lorsque les variables ne correspondent pas, on les crée avec des valeurs manquantes, via setdiff
rbind_alt <- function(x, y) {
  rbind(data.frame(c(x, sapply(setdiff(names(y), names(x)), function(z) NA))),
        data.frame(c(y, sapply(setdiff(names(x), names(y)), function(z) NA)))
  )
  }
empiler <- rbind_alt(notes, moyennes)
# On renomme la ligne des moyennes ajoutée
empiler[nrow(empiler), "identifiant"] <- "Moyenne"
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes par individu
moyennes <- donnees_tidyverse %>% 
  summarise(across(varNotes, ~mean(., na.rm = TRUE)))
empiler <- donnees_tidyverse %>% 
  select(all_of(varNotes)) %>% 
  bind_rows(moyennes)
# Fonctionne toujours si l'on concatène des bases de taille différente
empiler <- donnees_tidyverse %>% 
  select(identifiant, all_of(varNotes)) %>% 
  bind_rows(moyennes)
empiler <- empiler %>% 
  # On renomme la ligne des moyennes ajoutée
  mutate(identifiant = ifelse(row_number() == nrow(empiler),
                              "Moyenne",
                              identifiant))
# On va empiler la somme des notes en dessous de la base des notes
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
# Moyenne des notes par individu
moyennes <- data.table(donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), .SDcols = varNotes])
# On sélectionne la base des notes
notes <- donnees_datatable[, mget(c("identifiant", varNotes))]
empiler <- rbindlist(list(notes, moyennes), fill = TRUE)
# On renomme la ligne des moyennes ajoutée
set(empiler, i = nrow(empiler), j = "identifiant", value = "Moyenne")

14.11 Ajouter une ligne de valeurs manquantes à une base de données

data Ajout;run;
data Ajout_Missing;set Jointure Ajout;run;
ajout_na <- donnees_rbase
ajout_na[nrow(ajout_na) + 1, ] <- NA
ajout_na <- donnees_tidyverse %>%
  bind_rows(tibble(NA))
ajout_na <- rbindlist(list(donnees_datatable, data.table(NA)), fill = TRUE)

14.12 Semi join

/* Identifiants de la base de gauche qui ont un correspondant dans la base de droite */
proc sql;
  create table Semi_Join as select * from donnees_sas
  where Identifiant in (select distinct Identifiant from Diplome);
  select count(*) from Semi_Join;
quit;
proc sql;
  create table Semi_Join as select * from donnees_sas a
  where exists (select * from Diplome b where (a.Identifiant = b.Identifiant));
  select count(*) from Semi_Join;
quit;
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_rbase[donnees_rbase$identifiant %in% diplome_rbase$identifiant, ]
dim(semiJoin)
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_tidyverse %>% 
  semi_join(diplome_tidyverse, join_by(identifiant == identifiant))
dim(semiJoin)
# Autre solution
semiJoin <- semi_join(donnees_tidyverse, diplome_tidyverse, join_by(identifiant == identifiant))
dim(semiJoin)
# Identifiants de la base de gauche qui ont un correspondant dans la base de droite
semiJoin <- donnees_datatable[identifiant %in% diplome_datatable$identifiant, ]
dim(semiJoin)

14.13 Anti join

/* Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite */
proc sql;
  create table Anti_Join as select * from donnees_sas
  where Identifiant not in (select distinct Identifiant from Diplome);
  select count(*) from Anti_Join;
quit;
proc sql;
  create table Anti_Join as select * from donnees_sas a
  where not exists (select * from Diplome b where (a.Identifiant = b.Identifiant);
  select count(*) from Anti_Join;
quit;
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
antiJoin <- donnees_rbase[! donnees_rbase$identifiant %in% diplome_rbase$identifiant, ]
dim(antiJoin)
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
antiJoin <- donnees_tidyverse %>% 
  anti_join(diplome_tidyverse, join_by(identifiant == identifiant))
dim(antiJoin)
# Autre solution
antiJoin <- anti_join(donnees_tidyverse, diplome_tidyverse, join_by(identifiant == identifiant))
dim(antiJoin)
# Identifiants de la base de gauche qui n'ont pas de correspondant dans la base de droite
antiJoin <- donnees_datatable[! identifiant %in% diplome_datatable$identifiant, ]
dim(antiJoin)

14.14 Autres fonctions utiles

proc sql;
  /* Concaténation des identifiants */
  select Identifiant from Jointure union all
  select Identifiant from Diplome order by identifiant;
  /* Identifiants uniques des 2 bases */
  select distinct Identifiant from
  (select distinct Identifiant from Jointure union select distinct Identifiant from Diplome)
  order by identifiant;
  /* Identifiants communs des 2 bases */
  select Identifiant from Jointure intersect select Identifiant from Diplome
  order by identifiant;
  /* Identifiants dans jointure mais pas diplome */
  select distinct Identifiant from Jointure where
  Identifiant not in (select distinct Identifiant from Diplome)
  order by identifiant;
  select Identifiant from Jointure except select Identifiant from Diplome;
  /* Identifiants dans diplome mais pas jointure */
  select distinct Identifiant from Diplome
  where Identifiant not in (select distinct Identifiant from Jointure)
  order by identifiant;
  select Identifiant from Diplome except
  select Identifiant from Jointure order by identifiant;
quit;
# base:: permet de s'assurer que les fonctions proviennent de R-Base
# Des fonctions du même nom existent en Tidyverse, et tendent à prédominer si le package est lancé
# Concaténation des identifiants avec les doublons
sort(c(jointure_rbase$identifiant, diplome_rbase$identifiant))
# Identifiants uniques des 2 bases
sort(base::union(jointure_rbase$identifiant, diplome_rbase$identifiant))
sort(base::unique(c(jointure_rbase$identifiant, diplome_rbase$identifiant)))
# Identifiants communs des 2 bases
sort(base::intersect(jointure_rbase$identifiant, diplome_rbase$identifiant))
# Identifiants dans jointure mais pas diplome
sort(base::setdiff(jointure_rbase$identifiant, diplome_rbase$identifiant))
# Identifiants dans diplome mais pas jointure
sort(base::setdiff(diplome_rbase$identifiant, jointure_rbase$identifiant))
# dplyr:: permet de s'assurer que ce sont les fonctions du Tidyverse (et non leurs homonymes de R-Base qui sont utilisées)

# Concaténation des identifiants
dplyr::union_all(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Identifiants uniques des 2 bases
unique(dplyr::union_all(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant)) %>% 
  sort()
dplyr::union(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Identifiants communs des 2 bases
dplyr::intersect(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Identifiants dans jointure mais pas diplome
dplyr::setdiff(jointure_tidyverse$identifiant, diplome_tidyverse$identifiant) %>% 
  sort()
# Identifiants dans diplome mais pas jointure
dplyr::setdiff(diplome_tidyverse$identifiant, jointure_tidyverse$identifiant) %>% 
  sort()
# Les fonctions spécifiques à data.table fonctionnent avec des formats data.table, d'où la syntaxe un peu différente de R-Base

# Concaténation des identifiants
variable <- "identifiant"
sort(c(jointure_datatable[[variable]], diplome_datatable[[variable]]))
# Identifiants uniques des 2 bases
sort(unique(c(jointure_datatable[[variable]], diplome_datatable[[variable]])))
sort(union(jointure_datatable[[variable]], diplome_datatable[[variable]]))
# Identifiants communs des 2 bases
fintersect(jointure_datatable[, ..variable], diplome_datatable[, ..variable])[order(identifiant)]
# Identifiants dans jointure mais pas diplome
fsetdiff(jointure_datatable[, ..variable], diplome_datatable[, ..variable])[order(identifiant)]
# Identifiants dans diplome mais pas jointure
fsetdiff(diplome_datatable[, ..variable], jointure_datatable[, ..variable])[order(identifiant)]

15 Statistiques descriptives

15.1 Moyenne

proc means data = donnees_sas mean;var note_contenu;run;
proc sql;select mean(note_contenu) from donnees_sas;run;
# Importance du na.rm = TRUE
mean(donnees_rbase$note_contenu)
mean(donnees_rbase$note_contenu, na.rm = TRUE)
# Importance du na.rm = TRUE
donnees_tidyverse %>% pull(note_contenu) %>% mean()
donnees_tidyverse %>% pull(note_contenu) %>% mean(na.rm = TRUE)

# Autres solutions
# Le chiffre est arrondi lorsqu'il est affiché, du fait des propriétés des tibbles
donnees_tidyverse %>% summarise(mean(note_contenu, na.rm = TRUE))
donnees_tidyverse %>% 
  summarise(across(note_contenu, ~mean(., na.rm = TRUE)))

# Attention, en tidyverse, les syntaxes suivantes ne fonctionnent pas !
# donnees_tidyverse %>% mean(note_formateur)
# donnees_tidyverse %>% mean(note_formateur, na.rm = TRUE)
# Importance du na.rm = TRUE
donnees_datatable[, mean(note_contenu)]
donnees_datatable[, mean(note_contenu, na.rm = TRUE)]

15.2 Moyenne par sélection

/* Ici pour les seules femmes */
proc means data = donnees_sas mean;
  var note_contenu;
  where sexef = "Femme";
run;
# Ici, pour les seules femmes
with(subset(donnees_rbase, sexef == "Femme"), mean(note_contenu, na.rm = TRUE))
# Ici, pour les seules femmes
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  pull(note_contenu) %>%
  mean(na.rm = TRUE)

# Autres solutions
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  summarise(moyenne = mean(note_contenu, na.rm = TRUE))
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  summarise(across(note_contenu, ~ mean(., na.rm = TRUE)))
# Attention, syntaxe qui ne fonctionne qu'avec %>%, pas avec %>% !
donnees_tidyverse %>%
  filter(sexef == "Femme") %>% 
  {mean(.$note_contenu, na.rm = TRUE)}
# Ici, pour les seules femmes
donnees_datatable[sexef == "Femme", mean(note_contenu, na.rm = TRUE)]
donnees_datatable[sexef == "Femme", lapply(.SD, function(x) mean(x, na.rm = TRUE)), .SDcols = "note_contenu"]

15.3 Moyenne pondérée

/* Ensemble des données */
proc means data = donnees_sas mean;
  var note_contenu;
  weight poids_sondage;
run;
/* Par sélection (ici pour les seules femmes) */
proc means data = donnees_sas mean;
  var note_contenu;
  where sexef = "Femme";
  weight poids_sondage;
run;
# Ensemble des données
weighted.mean(donnees_rbase$note_contenu, donnees_rbase$poids_sondage, na.rm = TRUE)
# Autre méthode, mais attention aux NA !!
with(donnees_rbase, sum(note_contenu * poids_sondage, na.rm = TRUE) / sum((!is.na(note_contenu)) * poids_sondage, na.rm = TRUE))
# Par sélection (ici pour les seules femmes)
with(subset(donnees_rbase, sexef == "Femme"), sum(note_contenu * poids_sondage, na.rm = TRUE) / sum(poids_sondage, na.rm = TRUE))
# On peut aussi utiliser la fonction crossprod
# Mais ne fonctionne pas dans ce cas, car elle ne prend pas en compte les na.rm = TRUE
# Ne pas confondre cumprod et crossprod !!!!
with(subset(donnees_rbase, sexef == "Femme"), crossprod(note_contenu, poids_sondage) / sum(poids_sondage, na.rm = TRUE))
# Ensemble des données
donnees_tidyverse %>%
  summarise(across(note_contenu, ~weighted.mean(., w = poids_sondage, na.rm = TRUE)))
# Par sélection
donnees_tidyverse %>%
  filter(sexef == "Femme") %>%
  summarise(across(note_contenu, ~weighted.mean(., w = poids_sondage, na.rm = TRUE)))
# Ensemble des données
donnees_datatable[, weighted.mean(note_contenu, poids_sondage, na.rm = TRUE)]
# Par sélection (ici pour les seules femmes)
donnees_datatable[sexef == "Femme", weighted.mean(note_contenu, poids_sondage, na.rm = TRUE)]
donnees_datatable[sexef == "Femme", lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)),
                  .SDcols = "note_contenu"]

15.4 Moyenne de plusieurs variables

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
proc means data = donnees_sas mean;
  var &notes.;
run;
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# Plusieurs solutions
# Sous forme de liste
lapply(donnees_rbase[, notes], mean, na.rm = TRUE)
# Sous forme de vecteur
sapply(donnees_rbase[, notes], mean, na.rm = TRUE)
apply(donnees_rbase[, notes], 2, mean, na.rm = TRUE)
# Si l'on souhaite renommer les colonnes
moyennes <- sapply(donnees_rbase[, notes], mean, na.rm = TRUE)
names(moyennes) <- paste("Moyenne", names(moyennes), sep = "_")
moyennes
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_tidyverse %>%
  summarise_at(notes, mean, na.rm = TRUE)
# Autres solutions
donnees_tidyverse %>%
  summarise(across(all_of(notes), ~ mean(.x, na.rm = TRUE)))
# Obsolète
donnees_tidyverse %>%  
  select(starts_with("Note") & !ends_with("_100")) %>% 
  summarise_all(.funs = ~ mean(., na.rm = TRUE), .vars = notes)
# Si l'on souhaite renommer les colonnes
moyennes <- donnees_tidyverse %>%
  summarise_at(notes, mean, na.rm = TRUE) %>% 
  rename_with(~ paste("Moyenne", ., sep = "_"))
moyennes <- donnees_tidyverse %>%
  summarise(across(all_of(notes), ~ mean(.x, na.rm = TRUE), .names = "Moyenne_{.col}"))
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
moyennes <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), .SDcols = notes]
# Si l'on souhaite renommer les colonnes
setnames(moyennes, notes, paste("Moyenne", notes, sep = "_"))
moyennes

15.5 Moyenne pondérée de plusieurs variables

%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
proc means data = donnees_sas mean;
  var &notes.;
  weight poids_sondage;
run;
with(donnees_rbase, sapply(donnees_rbase[, notes], function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)))
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_tidyverse %>%
  summarise(across(notes, ~ weighted.mean(.x, poids_sondage, na.rm = TRUE)))
# Autre solution
donnees_tidyverse %>%
  summarise_at(notes, ~ weighted.mean(.x, poids_sondage, na.rm = TRUE))
moyennes <- donnees_datatable[, lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)), .SDcols = notes]
moyennes

15.6 Nombreuses statistiques (somme, moyenne, médiane, mode, etc.)

/* Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes */
/* Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données non manquantes (n),
   nombre de données manquantes (nmiss), intervalle, mode */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
/* Par la proc means, en un seul tableau */
proc means data = donnees_sas sum mean median min max var std n nmiss range mode;
  var &notes.;
run;
/* Par la proc univariate, variable par variable */
proc univariate data = donnees_sas;
  var &notes.;
run;
# Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données (manquantes et non manquantes),
# Nombre de valeurs manquantes, Intervalle
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
# Une solution pour obtenir le mode est d'utiliser fmode du package collapse
library(collapse)
sapply(donnees_rbase[, notes], function(x) c("Somme"      = sum(x, na.rm = TRUE),
                                             "Moyenne"    = mean(x, na.rm = TRUE),
                                             "Médiane"    = median(x, na.rm = TRUE),
                                             "Min"        = min(x, na.rm = TRUE),
                                             "Max"        = max(x, na.rm = TRUE),
                                             # Pour la variance, la somme des carrés est divisée par n - 1, où n est le nombre de données
                                             "Variance"   = var(x, na.rm = TRUE),
                                             "Ecart-type" = sd(x, na.rm = TRUE),
                                             "N"          = length(x),
                                             "NMiss"      = sum(is.na(x)),
                                             "Intervalle" = max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
                                             "Mode"       = collapse::fmode(x)
))
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
# Une solution pour obtenir le mode est d'utiliser fmode du package collapse
library(collapse)
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
StatsDesc_tidyverse <- function(x) {
  c(
    Somme      = sum(x, na.rm = TRUE),
    Moyenne    = mean(x, na.rm = TRUE),
    Mediane    = median(x, na.rm = TRUE),
    Min        = min(x, na.rm = TRUE),
    Max        = max(x, na.rm = TRUE),
    Variance   = var(x, na.rm = TRUE),
    Ecart_type = sd(x, na.rm = TRUE),
    N          = length(x),
    NMiss      = sum(is.na(x)),
    Intervalle = max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
    Mode       = collapse::fmode(x)
    )
}
# 1ère solution avec les notes en ligne et les statistiques en colonnes
donnees_tidyverse %>% 
  select(all_of(notes)) %>% 
  map(~ StatsDesc_tidyverse(.x)) %>% 
  bind_rows() %>% 
  bind_cols(tibble(Note = c(notes))) %>% 
  relocate(Note)
# 2e solution avec les notes en colonne
donnees_tidyverse %>%
  reframe(across(notes, ~ StatsDesc_tidyverse(.x))) %>% 
  bind_cols(tibble(Indicateur = c("Somme", "Moyenne", "Mediane", "Min", "Max", "Variance",
                                  "Ecart_type", "N", "NMiss", "Intervalle", "Mode"))) %>% 
  relocate(Indicateur)
# Petite différence avec SAS sur le nombre de lignes du fait des valeurs manquantes
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
# Une solution pour obtenir le mode est d'utiliser fmode du package collapse
library(collapse)
moyennes <- donnees_datatable[, lapply(.SD, function(x) c(sum(x, na.rm = TRUE),
                                                          mean(x, na.rm = TRUE),
                                                          median(x, na.rm = TRUE),
                                                          min(x, na.rm = TRUE),
                                                          max(x, na.rm = TRUE),
                                                          var(x, na.rm = TRUE),
                                                          sd(x, na.rm = TRUE),
                                                          .N,
                                                          sum(is.na(x)),
                                                          max(x, na.rm = TRUE) - min(x, na.rm = TRUE),
                                                          collapse::fmode(x)
                                                          )),
                              .SDcols = notes]
cbind(data.table(Nom = c("Somme", "Moyenne", "Médiane", "Min", "Max", "Variance", "Ecart_type", "N", "NMiss", "Intervalle", "Mode")), moyennes)
# Autre solution
StatsDesc <- function(x) {
  list(
    Variable   = names(x),
    Somme      = lapply(x, sum, na.rm = TRUE),
    Moyenne    = lapply(x, mean, na.rm = TRUE),
    Mediane    = lapply(x, median, na.rm = TRUE),
    Min        = lapply(x, min, na.rm = TRUE),
    Max        = lapply(x, max, na.rm = TRUE),
    Variance   = lapply(x, var, na.rm = TRUE),
    Ecart_type = lapply(x, sd, na.rm = TRUE),
    N          = lapply(x, function(x) length(x)),
    NMiss      = lapply(x, function(x) sum(is.na(x))),
    Intervalle = lapply(x, function(x) max(x, na.rm = TRUE) - min(x, na.rm = TRUE)),
    Mode       = lapply(x, collapse::fmode)
    )
}
donnees_datatable[, StatsDesc(.SD), .SDcols = notes]

15.7 Nombreuses statistiques pondérées (somme, moyenne, médiane, mode, etc.)

/* Somme, moyenne, médiane, minimum, maximum, variance, écart-type, nombre de données non manquantes (n),
   nombre de données manquantes (nmiss), intervalle, mode */
/* Par la proc means, en un seul tableau */
/* L'option vardef = wgt permet de diviser la variable par la somme des poids et non le nombre de données, pour être cohérent
   avec R */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
/* Par la proc means, en un seul tableau */
proc means data = donnees_sas sum mean median min max var std n nmiss range mode vardef = wgt;
  var &notes.;
  weight poids_sondage;
run;
/* Par la proc univariate, variable par variable */
proc univariate data = donnees_sas;
  var &notes.;
  weight poids_sondage;
run;
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)
sapply(donnees_rbase[, notes], function(x) c("Somme"      = collapse::fsum(x, w = donnees_rbase$poids_sondage),
                                             "Moyenne"    = collapse::fmean(x, w = donnees_rbase$poids_sondage),
                                             "Médiane"    = collapse::fmedian(x, w = donnees_rbase$poids_sondage),
                                             "Min"        = collapse::fmin(x),
                                             "Max"        = collapse::fmax(x),
                                             # Pour la variance, la somme des carrés est divisée par n - 1, où n est le nombre de données
                                             "Variance"   = collapse::fvar(x, w = donnees_rbase$poids_sondage),
                                             "Ecart-type" = collapse::fsd(x, w = donnees_rbase$poids_sondage),
                                             "N"          = collapse::fnobs(x),
                                             "NMiss"      = collapse::fnobs(is.na(x)),
                                             "Intervalle" = collapse::fmax(x) - collapse::fmin(x),
                                             "Mode"       = collapse::fmode(x, w = donnees_rbase$poids_sondage)
))
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
StatsDescPond_tidyverse <- function(x, w) {
  c(
    Somme      = collapse::fsum(x, w),
    Moyenne    = collapse::fmean(x, w),
    Mediane    = collapse::fmedian(x, w),
    Min        = collapse::fmin(x),
    Max        = collapse::fmax(x),
    Variance   = collapse::fvar(x, w),
    Ecart_type = collapse::fsd(x, w),
    N          = collapse::fnobs(x),
    NMiss      = collapse::fnobs(is.na(x)),
    Intervalle = collapse::fmax(x) - collapse::fmin(x),
    Mode       = collapse::fmode(x, w)
  )
}
# À FAIRE : Comment faire cela ???
# Tous les codes suivants ne fonctionnent pas !
#donnees_tidyverse %>%
#  reframe(across(notes, ~ StatsDescPond_tidyverse(.x, 10))) %>% 
#  bind_cols(tibble(Indicateur = c("Somme", "Moyenne", "Mediane", "Min", "Max", "Variance",
#                                  "Ecart_type", "N", "NMiss", "Intervalle", "Mode"))) %>% 
#  relocate(Indicateur)
#donnees_tidyverse %>% 
#  select(all_of(notes), poids_sondage) %>% 
#  map(all_of(notes), ~ StatsDescPond_tidyverse(.x, poids_sondage)) %>% 
#  bind_rows() %>% 
#  bind_cols(tibble(Note = c(notes))) %>% 
#  relocate(Note)
#donnees_tidyverse %>% 
#  select(all_of(notes), poids_sondage) %>% 
#  map(~StatsDescPond_tidyverse(.x, donnees_tidyverse$poids_sondage) )
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
# L'option na.rm est par défaut à TRUE dans le package
library(collapse)
# À FAIRE : y-a-t-il plus simple ???
# Est-on obligés d'utiliser systématiquement donnees_datatable$poids_sondage ?
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
StatsDescPond <- function(x) {
  list(
    Variables  = names(x),
    Somme      = lapply(x, collapse::fsum, w = donnees_datatable$poids_sondage),
    Moyenne    = lapply(x, collapse::fmean, w = donnees_datatable$poids_sondage),
    Mediane    = lapply(x, collapse::fmedian, w = donnees_datatable$poids_sondage),
    Min        = lapply(x, collapse::fmin),
    Max        = lapply(x, collapse::fmax),
    Variance   = lapply(x, collapse::fvar, w = donnees_datatable$poids_sondage),
    Ecart_type = lapply(x, collapse::fsd, w = donnees_datatable$poids_sondage),
    N          = lapply(x, collapse::fnobs),
    NMiss      = lapply(x, function(x) collapse::fnobs(is.na(x))),
    Intervalle = lapply(x, function(x) collapse::fmax(x) - collapse::fmin(x)),
    Mode       = lapply(x, collapse::fmode, w = donnees_datatable$poids_sondage)
  )
}
donnees_datatable[, StatsDescPond(.SD), .SDcols = notes]

15.8 Calcul d’une variance pondérée

proc means data = donnees_sas var vardef = wgt;
  var note_contenu;
  weight poids_sondage;
run;
library(collapse)
with(donnees_rbase, collapse::fvar(note_contenu, w = poids_sondage))
# On la calcule "manuellement" pour confirmer le résultat
# Formule : S_i{w_i * x_i**2} / S_i{w_i} - x_m**2
x_2 <- with(donnees_rbase,
            sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_m <- with(donnees_rbase,
            sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
p <- with(donnees_rbase,
          sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_2 / p - (x_m / p) ** 2
library(collapse)
donnees_tidyverse %>% 
  summarise(var = collapse::fvar(note_contenu, w = poids_sondage))
# On la calcule "manuellement" pour confirmer le résultat
# Formule : S_i{w_i * x_i**2} / S_i{w_i} - x_m**2
x_2 <- with(donnees_tidyverse,
            sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_m <- with(donnees_tidyverse,
            sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
p <- with(donnees_tidyverse,
          sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE))
x_2 / p - (x_m / p) ** 2
library(collapse)
donnees_datatable[, collapse::fvar(note_contenu, w = poids_sondage)]
# On la calcule "manuellement" pour confirmer le résultat
# Formule : S_i{w_i * x_i**2} / S_i{w_i} - x_m**2
x_2 <- donnees_datatable[, sum(poids_sondage * note_contenu**2 * complete.cases(note_contenu, poids_sondage),
                               na.rm = TRUE)]
x_m <- donnees_datatable[, sum(poids_sondage * note_contenu * complete.cases(note_contenu, poids_sondage),
                               na.rm = TRUE)]
p <- donnees_datatable[, sum(poids_sondage * complete.cases(note_contenu, poids_sondage), na.rm = TRUE)]
x_2 / p - (x_m / p) ** 2

15.9 Déciles et quartiles

/* On calcule déjà la moyenne des notes par individu */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  /* 1ère solution */
  Note_moyenne    = mean(of &notes.);
  /* 2e solution : l'équivalent des list-comprehension de Python en SAS */
  %macro List_comprehension;
    Note_moyenne2 = mean(of %do i = 1 %to %sysfunc(countw(&notes.));
                              %let j = %scan(&notes., &i.);
                              &j.
                            %end;);;
  %mend List_comprehension;
  %List_comprehension;
run;

/* Déciles et quartiles de la note moyenne */
/* Par la proc means */
proc means data = donnees_sas StackODSOutput Min P10 P20 P30 P40 Median P60 P70 Q3 P80 P90 Max Q1 Median Q3 QRANGE;
  var Note_moyenne;
  ods output summary = Deciles_proc_means;
run;
/* Par la proc univariate */
proc univariate data = donnees_sas;
  var Note_moyenne;
  output out = Deciles_proc_univariate pctlpts=00 to 100 by 10 25 50 75 PCTLPRE=_; 
run;
# on calcule déjà la moyenne des notes par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, notes], na.rm = TRUE)
# Et les quantiles
quantile(donnees_rbase$note_moyenne, probs = c(seq(0, 1, 0.1), 0.25, 0.75), na.rm = TRUE)
# Intervalle inter-quartile
IQR(donnees_rbase$note_moyenne, na.rm = TRUE)
# on calcule déjà la moyenne des notes par individu
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))
# Et les quantiles
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  quantile(probs = c(seq(0, 1, 0.1), 0.25, 0.75), na.rm = TRUE)
# Intervalle inter-quartile
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  IQR(na.rm = TRUE)
# on calcule déjà la moyenne des notes par individu
notes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
# On souhaite moyenner les notes par formation
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = notes]
donnees_datatable[, quantile(.SD, probs = c(seq(0, 1, 0.1), 0.25, 0.75), na.rm = TRUE), .SDcols = "note_moyenne"]
# Intervalle inter-quartile
donnees_datatable[, IQR(note_moyenne, na.rm = TRUE)]
donnees_datatable[, lapply(.SD, function(x) IQR(x, na.rm = TRUE)), .SDcols = "note_moyenne"]

15.10 Déciles et quartiles pondérés

/* On calcule déjà la moyenne des notes par individu */
%let notes = Note_Contenu Note_Formateur Note_Moyens Note_Accompagnement Note_Materiel;
data donnees_sas;
  set donnees_sas;
  Note_moyenne = mean(of &notes.);
run;
/* Par la proc means */
proc means data = donnees_sas StackODSOutput Min P10 P20 P30 P40 Median P60 P70 Q3 P80 P90 Max Q1 Median Q3 QRANGE;
  var Note_moyenne;
  ods output summary = Deciles_proc_means;
  weight poids_sondage;
run;
/* Par la proc univariate */
proc univariate data = donnees_sas;
  var Note_moyenne;
  output out = Deciles_proc_univariate pctlpts=00 to 100 by 10 25 50 75 PCTLPRE=_;
  weight poids_sondage;
run;
# Une solution pour obtenir les résultats pondérés est d'utiliser la fonction fquantile du package collapse
library(collapse)
# on calcule déjà la moyenne des notes par individu
notes <- tolower(c("Note_Contenu", "Note_Formateur", "Note_Moyens", "Note_Accompagnement", "Note_Materiel"))
donnees_rbase$note_moyenne <- rowMeans(donnees_rbase[, notes], na.rm = TRUE)
# Et les quantiles
# L'option na.rm est par défaut à TRUE dans le package
collapse::fquantile(donnees_rbase$note_moyenne, w = donnees_rbase$poids_sondage,
                    probs = c(seq(0, 1, 0.1)))
# Une solution pour obtenir les résultats pondérés est d'utiliser les fonctions du package collapse
library(collapse)
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE))
# Et les quantiles
# L'option na.rm est par défaut à TRUE dans le package
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  collapse::fquantile(probs = c(seq(0, 1, 0.1)), w = donnees_tidyverse$poids_sondage)
donnees_tidyverse %>%
  pull(note_moyenne) %>% 
  collapse::fquantile(probs = c(0, 0.25, 0.5, 0.75, 1), w = donnees_tidyverse$poids_sondage)
# Une solution pour obtenir les résultats pondérés est d'utiliser la fonction fquantile du package collapse
library(collapse)
# on calcule déjà la moyenne des notes par individu
notes <- c("note_contenu","note_formateur","note_moyens","note_accompagnement","note_materiel")
# On souhaite moyenner les notes par formation
# L'option na.rm est par défaut à TRUE dans le package
donnees_datatable[, note_moyenne := rowMeans(.SD, na.rm = TRUE), .SDcols = notes]
donnees_datatable[, lapply(.SD, function(x) collapse::fquantile(x, w = poids_sondage,
                                                                probs = c(seq(0, 1, 0.1))
                                                                )),
                           .SDcols = "note_moyenne"]

15.11 Rang de la note

/* Ajouter dans la base le rang de la note par ordre décroissant */
proc rank data = donnees_sas out = donnees_sas descending;
  var note_moyenne;
  ranks rang_note_moyenne;
run;
# Ajouter dans la base le rang de la note par ordre décroissant
donnees_rbase$rang_note_moyenne <- rank(-donnees_rbase$note_moyenne)
# Ajouter dans la base le rang de la note par ordre décroissant
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
donnees_tidyverse <- donnees_tidyverse %>%
  mutate(note_moyenne = rowMeans(across(all_of(varNotes)), na.rm = TRUE)) %>% 
  mutate(rang_note_moyenne = rank(-note_moyenne))
# Ajouter dans la base le rang de la note par ordre décroissant
# Attention, en R, rank trie par ordre croissant par défaut, alors que le tri est par ordre décroissant en SAS
# On exprime le rang par ordre décroissant, avec le - devant
donnees_datatable[, rang_note_moyenne := rank(-note_moyenne)]

15.12 Corrélation linéaire entre deux notes

/* Covariance et corrélation linéaire (Kendall, Pearson, Spearman) */
proc corr data = donnees_sas kendall pearson spearman cov;
  var note_contenu note_formateur;
run;
# Covariance (Kendall, Pearson, Spearman)
with(donnees_rbase,
     sapply(c("pearson", "spearman", "kendall"),
            function(x) cov(note_contenu, note_formateur, method = x, use = "complete.obs")))
# Corrélation linéaire (Kendall, Pearson, Spearman)
with(donnees_rbase,
     sapply(c("pearson", "spearman", "kendall"),
            function(x) cor(note_contenu, note_formateur, method = x, use = "complete.obs")))
# Covariance (Kendall, Pearson, Spearman)
# À FAIRE : peut-on faire mieux ???
methodes <- c("pearson", "spearman", "kendall")
methodes %>% 
  purrr::map(~ 
    donnees_tidyverse %>% 
      summarise(cov = cov(note_contenu, note_formateur, method = .x, use = "complete.obs"))) %>% 
  setNames(methodes) %>% 
  as_tibble()

# Corrélation linéaire (Kendall, Pearson, Spearman)
# À FAIRE : peut-on faire mieux ???
methodes <- c("pearson", "spearman", "kendall")
methodes %>% 
  purrr::map(~ donnees_tidyverse %>% 
               summarise(cor = cor(note_contenu, note_formateur, method = .x, use = "complete.obs"))) %>% 
  setNames(methodes) %>% 
  as_tibble()
# Covariance (Kendall, Pearson, Spearman)
methodes <- c("pearson", "spearman", "kendall")
setNames(donnees_datatable[, lapply(methodes,
                           function(x) cov(note_contenu, note_formateur,
                                           method = x,
                                           use = "complete.obs"))],
         methodes)
# Corrélation linéaire (Kendall, Pearson, Spearman)
setNames(donnees_datatable[, lapply(methodes,
                                    function(x) cor(note_contenu, note_formateur,
                                                    method = x,
                                                    use = "complete.obs"))],
         methodes)

16 Tableaux de fréquence (proc freq de SAS)

16.1 Tableaux de fréquence (proc freq) pour 1 variable

proc freq data = donnees_sas;
  tables Sexe CSP;
  format Sexe sexef. CSP $cspf.;
run;
# Tableaux de fréquence (proc freq) (sans les valeurs manquantes)
table(donnees_rbase$cspf)
table(donnees_rbase$sexef)
# Autre syntaxe, donnant une mise en forme différente
ftable(donnees_rbase$cspf)
# Pour enlever les "donnees_rbase$", on peut utiliser with pour se placer dans l'environnement de donnees_rbase
with(donnees_rbase, table(cspf))
# Pour les proportions
prop.table(table(donnees_rbase$cspf)) * 100
# Devient plus difficile si l'on souhaite plus (sommes et proportions cumulées par exemple)
freq <- setNames(as.data.frame(table(donnees_rbase$cspf)), c("cspf", "Freq"))
freq <- transform(freq, Prop = Freq / sum(Freq) * 100)
freq <- transform(freq,
                  Freq_cum = cumsum(Freq),
                  Prop_cum = cumsum(Prop))
freq
donnees_tidyverse %>% 
  count(cspf) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_tidyverse %>% 
  count(sexef) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
# Ou alors
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(n = n()) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop)
  )
# Tableaux de fréquence (proc freq) (avec les valeurs manquantes)
donnees_datatable[, table(cspf) ]
donnees_datatable[, table(sexef) ]
# Pour les proportions
donnees_datatable[, prop.table(table(cspf)) ] * 100
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf]
donnees_datatable[, {tot = .N; .SD[, .(frac = .N / tot * 100), keyby = cspf]} ]
# Autre façon d'utiliser les méthodes de data.table, avec les fréquences et proportions cumulés
tab <- data.table::dcast(donnees_datatable, cspf ~ ., fun = length)
colnames(tab)[colnames(tab) == "."] <- "Nombre"
tab[, Prop := lapply(.SD, function(col) col / sum(col) * 100), .SDcols = is.numeric]
tab[, c("Freq_cum", "Prop_cum") := list(cumsum(Nombre), cumsum(Prop))]

16.2 Tableaux de fréquence avec les valeurs manquantes

proc freq data = donnees_sas;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;
# Tableaux de fréquence (proc freq) (avec les valeurs manquantes)
table(donnees_rbase$cspf, useNA = "always")
prop.table(table(donnees_rbase$cspf, useNA = "always")) * 100
# Tableaux de fréquence (proc freq) (avec les valeurs manquantes)
table(donnees_rbase$cspf, useNA = "always")
prop.table(table(donnees_rbase$cspf, useNA = "always")) * 100
donnees_tidyverse %>% 
  count(cspf) %>% 
  mutate(prop = n / sum(n) * 100)
donnees_datatable[, table(cspf, useNA = "always") ]
donnees_datatable[, prop.table(table(cspf, useNA = "always"))] * 100
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf]
donnees_datatable[, {tot = .N; .SD[, .(frac = .N / tot * 100), keyby = cspf]} ]

16.3 Tableaux de fréquence trié par modalité la plus courante

proc freq data = donnees_sas order = freq;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;
freq <- setNames(as.data.frame(table(donnees_rbase$cspf)), c("cspf", "Freq"))
freq <- transform(freq, Prop = Freq / sum(Freq) * 100)
freq[order(-freq$Freq), ]
donnees_tidyverse %>% 
  count(cspf) %>% 
  arrange(desc(n)) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_tidyverse %>% 
  count(cspf, sort = TRUE) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_datatable[, .(Nombre = .N,
                      Pourcentage = .N / length(donnees_datatable[, cspf]) * 100),
                  keyby = cspf][order(-Nombre)]

16.4 Tableaux de fréquence avec la pondération

proc freq data = donnees_sas;
  tables Sexe CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
xtabs(poids_sondage ~ cspf, data = donnees_rbase, addNA = TRUE)
prop.table(xtabs(poids_sondage ~ cspf, data = donnees_rbase, addNA = TRUE))
donnees_tidyverse %>% 
  count(cspf, wt = poids_sondage) %>% 
  mutate(prop = n / sum(n) * 100,
         n_cum = cumsum(n),
         prop_cum = cumsum(prop))
donnees_datatable[, xtabs(poids_sondage ~ cspf, data = donnees_datatable, addNA = TRUE) ]
donnees_datatable[, prop.table(xtabs(poids_sondage ~ cspf, data = donnees_datatable, addNA = TRUE)) ]
donnees_datatable[, .(prop = sum(poids_sondage, na.rm = TRUE) / sum(donnees_datatable[, poids_sondage]) * 100), keyby = cspf]
donnees_datatable[, {tot = sum(poids_sondage, na.rm = TRUE); .SD[, .(prop = sum(poids_sondage, na.rm = TRUE) / tot * 100), by = cspf]} ]

16.5 Tableaux de contingence (proc freq) pour 2 variables

proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
run;

/* Tableau de contingence (tableau croisé) sans les proportions lignes, colonnes et totales */
proc freq data = donnees_sas;
  tables CSP * Sexe  / missing nofreq norow nocol;
  format Sexe sexef. CSP $cspf.;
run;
# Tableau simple
table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always")
# Tableau avec les sommes
addmargins(table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always"))
# Proportions
tab <- table(donnees_rbase$cspf, donnees_rbase$sexef, useNA = "always")
# Proportions par case
addmargins(prop.table(tab)) * 100
# Proportions par ligne
addmargins(prop.table(tab, margin = 1)) * 100
# Proportions par colonne
addmargins(prop.table(tab, margin = 2)) * 100

# Solution alternative, sans pondération
tab <- xtabs(~ cspf + sexef, data = donnees_rbase)
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100
# À FAIRE : ajouter une une ligne avec les sommes !
# Tableau de fréquence
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n(), .groups = "drop_last") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>%
  spread(sexef, prop) %>% 
  mutate(Total = rowSums(across(where(is.numeric)), na.rm = TRUE))

# Proportions par ligne
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n()) %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Proportions par colonne
donnees_tidyverse %>% 
  group_by(sexef, cspf) %>% 
  summarise(prop = n()) %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)


tab <- donnees_tidyverse %>% 
  count(cspf, sexef, wt = poids_sondage, name = "prop") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  # À FAIRE : pourrait-on sommer directement avec pivot_wider ? Argument values_fn?
  # Ne semble pas fonctionner, pourqoi ???
  pivot_wider(names_from = sexef, values_from = prop) %>% 
  # Somme par lignes
  mutate(Total = rowSums(across(where(is.numeric)), na.rm = TRUE))
# Autre solution avec rowwise : bien penser à c_across et non across !
#rowwise() %>% 
#mutate(Total = sum(c_across(where(is.numeric)), na.rm = T)) %>% 
#ungroup()
# Somme par colonnes
tab <- bind_rows(tab, tab %>% 
                   summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE)),
                             across(where(is.character), ~"Total"))
)
tab
# Tableau simple
donnees_datatable[, table(cspf, sexef, useNA = "always") ]
# Tableau avec les sommes
donnees_datatable[, addmargins(table(cspf, sexef, useNA = "always")) ]
# Proportions
tab <- donnees_datatable[, table(cspf, sexef, useNA = "always") ]
# Proportions par case
addmargins(prop.table(tab)) * 100
# Proportions par ligne
addmargins(prop.table(tab, margin = 1)) * 100
# Proportions par colonne
addmargins(prop.table(tab, margin = 2)) * 100

# Solution alternative, sans pondération
tab <- donnees_datatable[, xtabs(~ cspf + sexef, data = donnees_datatable) ]
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100

# Autre solution, avec les Grouping sets
tab <- data.table::cube(donnees_datatable, .(Nb = .N), by = c("cspf", "sexef"))
tab <- data.table::dcast(tab, cspf ~ sexef, value.var = "Nb")
# On harmonise le tableau
tab <- rbind(tab[2:nrow(tab)], tab[1,])
setcolorder(tab, c(setdiff(names(tab), "NA"), "NA"))
# On renomme la ligne et la colonne des totaux
tab[nrow(tab), 1] <- "Total"
names(tab)[which(names(tab) == "NA")] <- "Total"
tab

# Autre façon d'utiliser les méthodes de data.table
tab_prop <- data.table::dcast(donnees_datatable, cspf ~ sexef, fun.aggregate = length)
# Proportion par ligne
tab_prop[, .SD / Reduce(`+`, .SD), cspf]
# Proportion par colonne
cols <- unique(donnees_datatable[, (sexef)])
tab_prop[, (lapply(.SD, function(col) col / sum(col))), .SDcols = cols]

# Pour avoir les sommes lignes
# À FAIRE : ne marche pas, à revoir !
#tab_prop <- data.table::dcast(donnees_datatable, cspf ~ sexef, fun.aggregate = length)
#tab_prop[, Total := rowSums(.SD), .SDcols = is.numeric]
#tab_prop <- rbind(tab_prop, tab_prop[, c(cspf = "Total", lapply(.SD, sum, na.rm = TRUE)),
#                                     .SDcols = is.numeric],
#                  fill = TRUE)
#tab_prop[, (lapply(.SD, function(col) col / sum(col))), .SDcols = -1]
## Pour avoir les sommes colonnes
#tab[, sum(.SD), by = 1:nrow(tab), .SDcols = is.numeric]
#tab[, (lapply(.SD, function(col) col / sum(col))), .SDcols = -1]
#
## Autre solution plus pratique avec data.table
## Manipuler des formules sur R
#variable <- c("cspf", "sexef")
#formule <- as.formula(paste(paste(variable, collapse = " + "), ".", sep = " ~ "))
#tab_prop <- data.table::dcast(donnees_datatable, formule, fun.aggregate = length)
#colnames(tab_prop)[colnames(tab_prop) == "."] <- "total"
#tab_prop[, prop := total / sum(total)]
## Le tableau est remis sous forme croisée
#tab_prop <- dcast(tab_prop, cspf ~ sexef, value.var = c("prop"), fill = 0)

16.6 Tableau de contingence avec pondération

proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
tab <- xtabs(poids_sondage ~ cspf + sexef, data = donnees_rbase, addNA = TRUE)
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100
# Avec la fonction count
donnees_tidyverse %>% 
  count(cspf, sexef, wt = poids_sondage, name = "prop") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)
# Avec la fonction summarise
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = sum(poids_sondage, na.rm = TRUE)) %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)
# Avec ajout des sommes par ligne et colonne
tab <- donnees_tidyverse %>% 
  count(cspf, sexef, wt = poids_sondage, name = "prop") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  # À FAIRE : pourrait-on sommer directement avec pivot_wider ? Argument values_fn?
  # Ne semble pas fonctionner, pourqoi ???
  pivot_wider(names_from = sexef, values_from = prop) %>% 
  # Somme par lignes
  mutate(Total = rowSums(across(where(is.numeric)), na.rm = TRUE))
  # Autre solution avec rowwise : bien penser à c_across et non across !
  #rowwise() %>% 
  #mutate(Total = sum(c_across(where(is.numeric)), na.rm = T)) %>% 
  #ungroup()
# Somme par colonnes
tab <- bind_rows(tab, tab %>% 
                   summarise(across(where(is.numeric), sum, na.rm = TRUE),
                             across(where(is.character), ~"Total"))
            )
tab
tab <- donnees_datatable[, xtabs(poids_sondage ~ cspf + sexef, data = donnees_datatable, addNA = TRUE) ]
tab
addmargins(prop.table(tab)) * 100
addmargins(prop.table(tab, margin = 1), margin = 2) * 100
addmargins(prop.table(tab, margin = 2), margin = 1) * 100

16.7 Copier-coller le tableau dans un tableur (Excel, etc.)

/* Copier-coller le résultat sur la fenêtre html "Results Viewer" */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing chisq;
  format Sexe sexef. CSP $cspf.;
run;
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)
# Création d'un tableau
tab <- xtabs(~ cspf + sexef, data = donnees_rbase)
tab <- addmargins(prop.table(tab)) * 100

# Afficher de façon plus jolie un tableau
knitr::kable(tab)

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
kableExtra::kable_paper(kableExtra::kbl(tab), "hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)
# Création d'un tableau
tab <- donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(prop = n(), .groups = "drop_last") %>% 
  ungroup() %>% 
  mutate(prop = prop / sum(prop) * 100) %>% 
  spread(sexef, prop)

# Afficher de façon plus jolie un tableau
tab %>% knitr::kable()

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
tab %>% 
  knitr::kable() %>% 
  kableExtra::kable_paper("hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel
# On utilise les packages knitr et kableExtra
library(knitr)
library(kableExtra)
# Création d'un tableau
tab <- donnees_datatable[, xtabs(poids_sondage ~ cspf + sexef, data = donnees_datatable, addNA = TRUE) ]
tab <- 
addmargins(prop.table(tab)) * 100

# Afficher de façon plus jolie un tableau
knitr::kable(tab)

# Copier-coller le résultat vers Excel
# Il suffit d'appliquer ce code ....
kableExtra::kable_paper(kableExtra::kbl(tab), "hover", full_width = F)
# ... et de copier-coller le résultat de la fenêtre Viewer vers Excel

16.8 Tests d’associaton (Chi-Deux, etc.)

proc freq data = donnees_sas;
  tables Sexe * CSP / missing chisq;
  format Sexe sexef. CSP $cspf.;
run;
# Test du Khi-Deux
with(donnees_rbase, chisq.test(cspf, sexef))
summary(table(donnees_rbase$cspf, donnees_rbase$sexef))
# Test du Khi-Deux
#donnees_tidyverse %>%
#  summarise(a = chisq.test(sexef, cspf))
# À FAIRE : fonctionne, mais pas vraiment Tidyverse
with(donnees_tidyverse, chisq.test(cspf, sexef))
# Test du Khi-Deux
donnees_datatable[, chisq.test(cspf, sexef)]

16.9 Solutions avec package R permettant de pondérer

Des informations sur l’usage des packages en R sont disponibles sur le site Utilit’R : https://book.utilitr.org/03_Fiches_thematiques/Fiche_comment_choisir_un_package.html.

/* Sans objet pour SAS */
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# 5 packages paraissent pertinents : descr, flextable, questionr, survey, procs
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# 5 packages paraissent pertinents : descr, flextable, questionr, survey, procs
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# 5 packages paraissent pertinents : descr, flextable, questionr, survey, procs

16.10 Package descr

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(descr)
# Non pondéré
with(donnees_rbase, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Pondéré
with(donnees_rbase, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Sans les proportions par ligne et colonne
with(donnees_rbase, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE))
# À FAIRE : est-il possible d'appliquer le pipe ???
library(descr)
# Non pondéré
with(donnees_tidyverse, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Pondéré
with(donnees_tidyverse, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Sans les proportions par ligne et colonne
with(donnees_tidyverse, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE))
library(descr)
# Non pondéré
with(donnees_datatable, descr::crosstab(cspf, sexef,                         prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Pondéré
with(donnees_datatable, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = TRUE, prop.c = TRUE, prop.t = TRUE))
# Sans les proportions par ligne et colonne
with(donnees_datatable, descr::crosstab(cspf, sexef, weight = poids_sondage, prop.r = FALSE, prop.c = FALSE, prop.t = TRUE))

16.11 Package flextable

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_rbase, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_rbase, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_rbase, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE,
                     include.column_percent = FALSE)
# À FAIRE : est-il possible d'appliquer le pipe ???
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_tidyverse, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE,
                     include.column_percent = FALSE)
library(flextable)
# Non pondéré
flextable::proc_freq(donnees_datatable, "cspf", "sexef")
# Pondéré
flextable::proc_freq(donnees_datatable, "cspf", "sexef", weight = "poids_sondage")
# Sans les proportions par ligne et colonne
flextable::proc_freq(donnees_datatable, "cspf", "sexef", weight = "poids_sondage", include.row_percent = FALSE,
                     include.column_percent = FALSE)

16.12 Package questionr

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
# Sans pondération
library(questionr)
# Tableau croisé
# Sans pondération
tab <- with(donnees_rbase, questionr::wtd.table(cspf, sexef, useNA = "ifany"), na.rm = TRUE)
# Avec pondération
tab <- with(donnees_rbase, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany"), na.rm = TRUE)
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)
# Sans pondération
library(questionr)
# Tableau croisé
# Sans pondération
tab <- with(donnees_tidyverse, questionr::wtd.table(cspf, sexef, useNA = "ifany"), na.rm = TRUE)
# Avec pondération
tab <- with(donnees_tidyverse, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany"), na.rm = TRUE)
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)
# Sans pondération
library(questionr)
# Tableau croisé
# Sans pondération
tab <- with(donnees_datatable, questionr::wtd.table(cspf, sexef, useNA = "ifany"), na.rm = TRUE)
# Avec pondération
tab <- with(donnees_datatable, questionr::wtd.table(cspf, sexef, weights = poids_sondage, useNA = "ifany"), na.rm = TRUE)
tab
# Proportions
questionr::prop(tab)
# Proportions colonnes
questionr::cprop(tab)
# Proportions lignes
questionr::rprop(tab)

16.13 Package survey

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
# À FAIRE : ne fonctionne pas
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_rbase)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)
# La syntaxe avec pipe n'est pas compatible avec le package survey
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_tidyverse)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)
# À FAIRE : ne fonctionne pas
library(survey)
tab <- survey::svydesign(id = ~1, weights = ~poids_sondage, data = donnees_datatable)
survey::svytable(poids_sondage ~ sexef + cspf, design = tab)

16.14 Package procs

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
library(procs)
procs::proc_freq(donnees_rbase, tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(donnees_rbase, tables = cspf * sexef, weight = poids_sondage, options = v(missing))
library(procs)
procs::proc_freq(donnees_tidyverse, tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(donnees_tidyverse, tables = cspf * sexef, weight = poids_sondage, options = v(missing))
library(procs)
# Il semble nécessaire de convertire l'objet en data.frame
procs::proc_freq(setDF(donnees_datatable), tables = cspf * sexef, options = v(missing))
# Ne fonctionne pas avec le poids !!!
#procs::proc_freq(setDF(donnees_datatable), tables = cspf * sexef, weight = poids_sondage, options = v(missing))
# On reconvertit en data.table
setDT(donnees_datatable)

16.15 Solutions avec package R ne permettant apparemment pas de pondérer

/* Sans objet pour SAS */
proc freq data = donnees_sas;
  tables Sexe * CSP / missing;
  format Sexe sexef. CSP $cspf.;
  weight poids_sondage;
run;
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# Autres packages, qui semblent quasi-inutiles, car ils ne permettent apparemment pas de pondérer

# Package janitor
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- janitor::tabyl(donnees_rbase, cspf, sexef)
tab
janitor::adorn_totals(tab, c("row", "col"))
# Pourcentages
janitor::adorn_percentages(tab, denominator = "all", na.rm = TRUE)
# Pourcentages lignes
janitor::adorn_percentages(tab, denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
janitor::adorn_percentages(tab, denominator = "col", na.rm = TRUE)

# Package crosstable
library(crosstable)
crosstable::crosstable(donnees_rbase, cspf, by = sexef, showNA = "always", percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")

# Package gmodels
library(gmodels)
gmodels::CrossTable(donnees_rbase$cspf, donnees_rbase$sexef)

# Package gtsummary
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("cell"),   margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("column"), margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_rbase, row = cspf, col = sexef, percent = c("row"),    margin = c("column", "row"), missing = c("always"))
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# Autres packages, qui semblent quasi-inutiles, car ils ne permettent apparemment pas de pondérer

# Package janitor
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- donnees_tidyverse %>% 
  janitor::tabyl(cspf, sexef) %>% 
  janitor::adorn_totals(c("row", "col"))
tab
# Pourcentages
tab %>% janitor::adorn_percentages(denominator = "all", na.rm = TRUE)
# Pourcentages lignes
tab %>% janitor::adorn_percentages(denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
tab %>% janitor::adorn_percentages(denominator = "col", na.rm = TRUE)

# Package crosstable
library(crosstable)
crosstable::crosstable(donnees_tidyverse, cspf, by = sexef, showNA = "always",
                       percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")

# Package gmodels
library(gmodels)
donnees_tidyverse %>% 
  summarise(gmodels::CrossTable(cspf, sexef))

# Package gtsummary
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("cell"),   margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("column"), margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_tidyverse, row = cspf, col = sexef, percent = c("row"),    margin = c("column", "row"), missing = c("always"))
# Autre possibilité, avec packages, pour avoir la même présentation que la proc freq de SAS
# Autres packages, qui semblent quasi-inutiles, car ils ne permettent apparemment pas de pondérer

# Package janitor
library(janitor)
# Attention, la fonction tabyl ne permet pas de pondérer
tab <- janitor::tabyl(donnees_datatable, cspf, sexef)
tab
janitor::adorn_totals(tab, c("row", "col"))
# Pourcentages
janitor::adorn_percentages(tab, denominator = "all", na.rm = TRUE)
# Pourcentages lignes
janitor::adorn_percentages(tab, denominator = "row", na.rm = TRUE)
# Pourcentages colonnes
janitor::adorn_percentages(tab, denominator = "col", na.rm = TRUE)

# Package crosstable : lui non plus ne permet pas de pondérer, apparemment ...
library(crosstable)
crosstable::crosstable(donnees_datatable, cspf, by = sexef, showNA = "always",
                       percent_digits = 0, percent_pattern ="{n} ({p_col}/{p_row})")

# Package gmodels : ne permet pas de pondérer, apparemment
library(gmodels)
gmodels::CrossTable(donnees_datatable$cspf, donnees_datatable$sexef)

# Package gtsummary : ne permet pas de pondérer, apparemment
library(gtsummary)
# Pourcentages par case, colonne, ligne
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("cell"),  
                     margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("column"),
                     margin = c("column", "row"), missing = c("always"))
gtsummary::tbl_cross(data = donnees_datatable, row = cspf, col = sexef, percent = c("row"),   
                     margin = c("column", "row"), missing = c("always"))

17 Statistiques agrégées par groupe

17.1 Une seule variable de groupement, une seule variable

/* Moyenne de note_contenu et nombre de personnes */
/* 1ère solution */
proc sort data = donnees_sas;by cspf;run;
proc means data = donnees_sas mean n;var note_contenu;class cspf;run;
/* 2e solution */
proc tabulate data = donnees_sas;
  var note_contenu;
  class cspf;
  table (cspf all = "Total"), note_contenu * (mean n);
run;
/* 3e solution */
proc sql;
  select cspf, mean(note_contenu) as note_contenu_moyenne, count(*) as N
  from donnees_sas
  group by cspf
  order by cspf;
quit;
/* Avec la pondération */
proc sort data = donnees_sas;by cspf;run;
proc means data = donnees_sas mean n;
  var note_contenu;class cspf;
  weight poids_sondage;
run;
proc tabulate data = donnees_sas;
  var note_contenu;
  class cspf;
  weight poids_sondage;
  table (cspf all = "Total"), note_contenu * (mean n);
run;
# Moyenne de note_contenu et nombre de personnes
aggregate(note_contenu ~ cspf, donnees_rbase, function(x) c(Moyenne = mean(x, na.rm = TRUE), Nombre = length(x)))
# Moyenne de note_contenu
# Une seule variable, une seule variable de groupe, une seule fonction
aggregate(note_contenu ~ cspf, donnees_rbase, mean, na.rm = TRUE)
# rowsum, à ne pas confondre avec rowSums, calcule des sommes, et uniquement des sommes
rowsum(donnees_rbase$note_contenu, donnees_rbase$cspf, recorder = TRUE, na.rm = TRUE)
# Fonctions tapply et by
tapply(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)
with(donnees_rbase, tapply(note_contenu, cspf, mean, na.rm = TRUE))
tapply(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)
by(donnees_rbase$note_contenu, donnees_rbase$cspf, mean, na.rm = TRUE)

# Avec la pondération : tapply ne fonctionne pas, il faut découper la base en facteurs avec split
sapply(split(donnees_rbase, donnees_rbase$cspf), function(x) weighted.mean(x$note_contenu, x$poids_sondage, na.rm = TRUE))
# À FAIRE : autre solution ?
# Moyenne de note_contenu et nombre de personnes
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Nombre = n(), Moyenne = mean(note_contenu, na.rm = TRUE))
# Moyenne de note_contenu
# Une seule variable, une seule variable de groupe, une seule fonction
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE))

# Avec la pondération
donnees_tidyverse %>% 
  group_by(cspf) %>% 
  summarise(Moyenne = weighted.mean(note_contenu, poids_sondage, na.rm = TRUE))
# Moyenne de note_contenu et nombre de personnes
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), by = cspf]
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), keyby = "cspf"]
# Variables définies à part
varNotes <- "note_contenu"
var_groupe <- "cspf"
# À FAIRE : les deux variables sont empilées, pourquoi ??
donnees_datatable[, lapply(.SD, function(x) list(moyenne = mean(x, na.rm = TRUE), n = length(x))), keyby = var_groupe,
                  .SDcols = varNotes]
# Avec la pondération
donnees_datatable[, lapply(.SD, function(x) weighted.mean(x, poids_sondage, na.rm = TRUE)), keyby = var_groupe,
                  .SDcols = varNotes]

17.2 Plusieurs variables

%let var_notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
%let var_groupe = cspf sexef;
proc sort data = donnees_sas;by &var_groupe.;run;
proc means data = donnees_sas mean n;
  class &var_groupe.;
  var &var_notes.;
  output out = Resultat;
run;
/* Autre solution */
%macro sel;
  %global select;
  %local i j;
  %let select = ;
  %do i = 1 %to %sysfunc(countw(&var_notes.));
    %let j = %scan(&var_notes., &i., %str( ));
    %let select = &select. mean(&j) as &j._moyenne,;
  %end;
%mend sel;
%sel;
%let group = %sysfunc(tranwrd(&var_groupe., %str( ), %str(, )));
proc sql;
  select &group., &select. count(*) as N
  from donnees_sas
  group by &group.
  order by &group.;
quit;
# Plusieurs solutions avec aggregate (plutôt lent)
aggregate(note_contenu ~ cspf + sexef, donnees_rbase, function(x) c(mean = mean(x), n = length(x)))
aggregate(cbind(note_contenu, note_materiel) ~ cspf + sexef, donnees_rbase, function(x) c(moyenne = mean(x, na.rm = TRUE), n = length(x)))
# Via les formules
variable <- c("note_contenu")
varGroupement <- c("cspf", "sexef")
formule <- as.formula(paste(variable, paste(varGroupement, collapse = " + "), sep = " ~ "))
aggregate(formule, donnees_rbase, function(x) c(moyenne = mean(x, na.rm = TRUE), n = length(x)))
# Avec by
by(donnees_rbase[, variable], donnees_rbase[, varGroupement], function(x) c(mean = mean(x, na.rm = TRUE), n = length(x)))
# Avec rowum : on ne peut calculer que la somme
rowsum(donnees_rbase[, variable], interaction(donnees_rbase[, varGroupement], sep = "_", lex.order = TRUE))
donnees_tidyverse %>% 
  group_by(cspf, sexef) %>% 
  summarise(Moyenne = mean(note_contenu, na.rm = TRUE), n = n())
donnees_datatable[, .(note_contenu_moyenne = mean(note_contenu, na.rm = TRUE), N = .N), keyby = c("cspf", "sexef")]
# Autre solution
data.table::dcast(donnees_datatable, cspf + sexef ~ ., value.var = "note_contenu", fun.aggregate = mean, na.rm = TRUE)
# Variables définies à part
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
# À FAIRE : les deux variables sont empilées, pourquoi ??
donnees_datatable[, lapply(.SD, function(x) list(moyenne = mean(x, na.rm = TRUE), n = length(x))),
                  keyby = var_groupe,
                  .SDcols = varNotes]

# Nombre de femmes par CSP
# Il y a un recycling de gender = "M", utile de le mentionner
donnees_datatable[, .(Femmes = sum(sexef == "Femme", na.rm = TRUE), Hommes = sum(sexef == "Homme", na.rm = TRUE)), by = .(cspf)]

# À FAIRE :
# Exemple avec les variables dans .SDcols
# data.table::setDT(DF)[, lapply(.SD, mean, na.rm = TRUE), .SDcols = c("x", "y"), by = list(g, h)]
# D'autres variations (par exemple, c(x, y) ou list("x", "y") ne fonctionnent pas !)

17.3 Tableaux croisés à 2 variables de groupement

proc tabulate data = donnees_sas;
  class cspf sexef;
  var note_contenu;
  table (cspf all = "Ensemble"), sexef * (note_contenu) * mean;
run;
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- c("note_contenu")

# Solution avec tapply
tapply(donnees_rbase[, variable], donnees_rbase[varGroupement], function(x) moyenne = mean(x, na.rm = TRUE))

# Solution avec xtabs
xtabs(note_contenu ~ cspf + sexef, aggregate(note_contenu ~ cspf + sexef, data = donnees_rbase, FUN = mean, na.rm = TRUE))
# Ou, sous forme de formule
formule <- as.formula(paste(variable, paste(varGroupement, collapse = " + "), sep = " ~ "))
xtabs(formule, aggregate(formule, data = donnees_rbase, FUN = mean, na.rm = TRUE))

# Solution avec aggregate, en calculant un tableau "long" et en le transformant en "wide"
tableau <- aggregate(note_contenu ~ cspf + sexef, data = donnees_rbase, FUN = mean, na.rm = TRUE)
tableau <- reshape(tableau, 
        timevar = varGroupement[2],
        idvar = varGroupement[1],
        direction = "wide")
tableau[is.na(tableau)] <- 0
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- c("note_contenu")
donnees_tidyverse %>% 
  group_by(across(all_of(varGroupement))) %>% 
  summarise(across(all_of(variable), ~ mean(.x, na.rm = TRUE), .names = "Moyenne")) %>% 
  spread(varGroupement[2], Moyenne)

# Autre solution
donnees_tidyverse %>% 
  group_by(!!!syms(varGroupement)) %>% 
  summarise(Moyenne = mean(.data[[variable]], na.rm = TRUE)) %>% 
  spread(varGroupement[2], Moyenne)
# Tableau croisé Cspf par Sexef
varGroupement <- c("cspf", "sexef")
variable <- "note_contenu"
data.table::dcast(donnees_datatable, cspf ~ sexef, value.var = "note_contenu", fun.aggregate = mean, na.rm = TRUE)

# Avec références seulement
data.table::dcast(donnees_datatable, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = variable,
                  fun.aggregate = mean, na.rm = TRUE)

# Autre solution, plus indirecte
# À FAIRE : attention, toujours utiliser lapply, même avec une seule variable ! LE DIRE !!!
tab <- donnees_datatable[, lapply(.SD, mean, na.rm = TRUE), keyby = varGroupement, .SDcols = "note_contenu"]
data.table::dcast(tab, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = variable)

17.4 Tableaux croisés à 3 variables de groupement ou plus : 1 variable en ligne, 2 en colonne

/* Notes par croisement de CSP (en ligne) et de Sexe x Niveau */
%let notes = note_contenu note_formateur note_moyens note_accompagnement note_materiel;
proc tabulate data = donnees_sas;
  class cspf sexef;
  var &notes.;
  table (cspf all = "Ensemble"), sexef * (&notes.) * mean;
run;

/* Note_contenu par croisement de CSP (en ligne) et de Sexe x Niveau */
proc tabulate data = donnees_sas;
  class cspf sexef Niveau;
  var note_moyenne;
  table (cspf all = "Ensemble"), (sexef * Niveau) * (note_moyenne) * mean;
run;
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")

tableau <- aggregate(donnees_rbase[, varNotes], donnees_rbase[var_groupe], function(x) moyenne = mean(x, na.rm = TRUE))
reshape(tableau, 
        timevar = var_groupe[2],
        idvar = var_groupe[1],
        direction = "wide")

# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
# À FAIRE : proposer une fonction ?
formule <- as.formula("note_contenu ~ cspf + sexef + niveau")
tab <- xtabs(formule, aggregate(formule, data = donnees_rbase, FUN = mean, na.rm = TRUE))
nomsCol <- do.call(paste, c(expand.grid(dimnames(tab)[-1L]), sep = "_"))
nomsLig <- dimnames(tab)[[1L]]
# Transformation du tableau de résultats (en format array) vers un format matrix, puis dataframe
# Permet d'exprimer le array (matrice multidimensionnelle) en un tableau à deux dimensions
# On transforme le tableau en matrice ayant en nombre de lignes dim(tab)[1], c'est-à-dire le nombre de lignes du array
# et en nombre de colonnes le reste des variables
tab <- data.frame(matrix(tab, nrow = dim(tab)[1L]))
# Renommage des noms des colonnes de la base
colnames(tab) <- nomsCol
# Renommage des noms des lignes de la base
row.names(tab) <- nomsLig
# On annule les valeurs manquantes
tab[is.na(tab)] <- 0
tab
# À FAIRE : développer autour de cet exemple
# Avec 3 variables
xtabs(cbind(note_contenu, note_materiel) ~ cspf + sexef, donnees_rbase)
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
donnees_tidyverse %>% 
  group_by(across(all_of(var_groupe))) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE))) %>% 
  pivot_wider(names_from = sexef,
              values_from = all_of(varNotes))



# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
varNotes <- c("note_contenu")
var_groupe <- c("cspf", "sexef", "niveau")
donnees_tidyverse %>% 
  group_by(across(all_of(var_groupe))) %>% 
  summarise(across(all_of(varNotes), ~ mean(.x, na.rm = TRUE))) %>% 
  pivot_wider(names_from = c(sexef, niveau),
              values_from = all_of(varNotes),
              values_fill = 0)
# 1er exemple : CSPF en ligne, et chacune des 5 notes croisées avec le sexe en colonne
varNotes <- c("note_contenu", "note_formateur", "note_moyens", "note_accompagnement", "note_materiel")
var_groupe <- c("cspf", "sexef")
data.table::dcast(donnees_datatable, get(varGroupement[1]) ~ get(varGroupement[2]), value.var = varNotes,
                  fun.aggregate = mean, na.rm = TRUE)

# 2e exemple : CSPF en ligne, et croisement Sexe x Qualifié en colonne, note_contenu sommée
data.table::dcast(donnees_datatable, cspf ~ sexef + niveau, value.var = "note_contenu",
                  fun.aggregate = mean, na.rm = TRUE)

18 Boucles

18.1 Boucles imbriquées

data _null_;call symput('annee', strip(year(today())));run;
/* Ensemble des premiers jours de chaque mois entre 2020 et le 31 décembre de l'année courante */
%macro Boucles_Imbriquees(an_debut, an_fin);
  %local i j;
  %global liste_mois;
  %let liste_mois = ;
  %do i = &an_debut. %to &an_fin.;
    %do j = 1 %to 12;
      %let liste_mois = &liste_mois. %sysfunc(putn(%sysfunc(mdy(&j., 1, &i.)), ddmmyy10.));
    %end;
  %end;
%mend Boucles_Imbriquees;
%let annee = %sysfunc(year(%sysfunc(today())));
%Boucles_Imbriquees(an_debut = 2020, an_fin = &annee.);
%put &liste_mois.;
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())
# 1ère solution avec for (lente, à déconseiller !)
listeMois <- c()
for (i in seq(2020, annee)) {
  for (j in 1:12) {
    listeMois <- as.Date(c(listeMois, lubridate::ymd(sprintf("%02d-%02d-01", i, j))), origin = "1970-01-01")
  }
}
# 2e  solution : 2 fonctions lapply imbriquées
listeMois <- as.Date(unlist(lapply(seq(2020, annee), 
                                   function(x) lapply(1:12, function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y))))),
                     origin = "1970-01-01")
# 3e solution : expand.grid
listeMois <- sort(as.Date(apply(expand.grid(seq(2020, annee), 1:12), 1, 
                                function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))),
                          origin = "1970-01-01"))
# 4e solution, la plus simple !
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())
# 1ère solution : 2 fonctions map imbriquées
listeMois <- purrr::map(seq(2020, annee), 
                        function(x) purrr::map(1:12,
                                               function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y)))) %>% 
  unlist() %>% 
  as.Date(, origin = "1970-01-01")

# 2e solution : expand_grid
listeMois <- tidyr::expand_grid(annee = seq(2020, annee), mois = 1:12) %>% 
  apply(1, function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))) %>% 
  as.Date(, origin = "1970-01-01") %>% 
  sort()

# 3e solution, la plus simple
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")
# Ensemble des premiers jours de chaque mois entre 2020 et l'année courante
annee <- lubridate::year(Sys.Date())
# 1ère solution avec for (lente, à déconseiller !)
listeMois <- c()
for (i in seq(2020, annee)) {
  for (j in 1:12) {
    listeMois <- as.Date(c(listeMois, lubridate::ymd(sprintf("%02d-%02d-01", i, j))), origin = "1970-01-01")
  }
}
# 2e  solution : 2 fonctions lapply imbriquées
listeMois <- as.Date(unlist(lapply(seq(2020, annee), 
                                   function(x) lapply(1:12, function(y) lubridate::ymd(sprintf("%02d-%02d-01", x, y))))),
                     origin = "1970-01-01")
# 3e solution : expand.grid
listeMois <- sort(as.Date(apply(expand.grid(seq(2020, annee), 1:12), 1, 
                                function(x) lubridate::ymd(sprintf("%02d-%02d-01", x[1], x[2]))),
                          origin = "1970-01-01"))
# 4e solution, la plus simple
seq.Date(lubridate::ymd(sprintf("%02d-01-01", 2020)), lubridate::ymd(sprintf("%02d-12-01", annee)), by = "month")

18.2 Boucles imbriquées (second exemple)

/* Itérer sur toutes les années et les trimestres d'une certaine plage */
/* on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 */
%macro iteration(debut, fin);
  %global liste_an;
  %let liste_an = ;
  %do i = &debut. %to &fin.;
    %let liste_an = &liste_an.&i.-;
  %end;
%mend iteration;
%iteration(debut = 2020, fin = %sysfunc(year(%sysfunc(today()))));
%put &liste_an.;
%let liste_trim = 1 2 3 4;
%let liste_niv = max min;
/* Supposons que nous ayons des noms de fichier suffixés par AXXXX_TY_NZ, avec X l'année, Y le trimestre et
   Z max ou min. Par exemple, A2010_T2_NMax */
/* Pour obtenir l'ensemble de ces noms de 2010 à cette année */
%macro noms_fichiers(base = temp);
  %global res;
  %let res = ;
  %do j = 1 %to %sysfunc(countw(&liste_an., "-"));
    %let y = %scan(&liste_an., &j., "-"); /* année */
    %do i = 1 %to 4;
      %let t = %scan(&liste_trim, &i.); /* trimestre */
      %do g = 1 %to 2;
        %let n = %scan(&liste_niv., &g.); /* niveau */
        %let res = &res. &base._&y._t&t._n&n.;
      %end;
    %end;
  %end;
%mend noms_fichiers;
%noms_fichiers(base = base);
%put &res.;
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
res <- unlist(lapply(debut:fin, function(x) lapply(c("max", "min"), function(y)  sprintf("base_%4d_t%d_n%s", x, 1:4, y))))
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
listeMois <- purrr::map(debut:fin, 
                        function(x) purrr::map(c("max", "min"),
                                               function(y) sprintf("base_%4d_t%d_n%s", x, 1:4, y))) %>% 
                          unlist()
# Itérer sur toutes les années et les trimestres d'une certaine plage
# on va afficher les noms base_AAAA_tT_nmax où AAAA désigne les années, T les trimestres, depuis 2020 
debut <- 2020
fin <- lubridate::year(Sys.Date())
res <- unlist(lapply(debut:fin, function(x) lapply(c("max", "min"), function(y)  sprintf("base_%4d_t%d_n%s", x, 1:4, y))))

18.3 Boucles for

/* On va créer une base par année d'entrée */
proc sql noprint;
  select year(min(date_entree)), year(max(date_entree)) into :an_min, :an_max
  from donnees_sas;
quit;
%macro Base_par_mois(debut, fin);
  /* %local impose que an n'est pas de signification hors de la macro */
  %local an;
  /* %global impose que nom_bases peut être utilisé en dehors de la macro */
  %global nom_bases;
  /* On initalise la création de la macri-variable nom_bases */
  %let nom_bases = ;
  /* On itère entre &debut. et &fin. */
  %do an = &debut. %to &fin.;
    data Entree_&an.;
      set donnees_sas;
      if year(date_entree) = &an.;
    run;
    /* On ajoute à la macro-variable le nom de la base */
    %let nom_bases = &nom_bases. Entree_&an.;
  %end;
%mend Base_par_mois;
%Base_par_mois(debut = &an_min., fin = &an_max.);
%put &nom_bases.;

/* On va désormais empiler toutes les bases (concaténation par colonne) */
/* L'instruction set utilisée de cette façon permet cet empilement */
data concatene;
  set &nom_bases.;
run;
# On va créer une base par année d'entrée
anMin <- min(lubridate::year(donnees_rbase$date_entree), na.rm = TRUE)
anMax <- max(lubridate::year(donnees_rbase$date_entree), na.rm = TRUE)
for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"), donnees_rbase[which(lubridate::year(donnees_rbase$date_entree) == i), ])
}
# On va désormais empiler toutes les bases (concaténation par colonne)
# do.call applique la fonction rbind à l'ensemble des bases issues du lapply
# get permet de faire le chemin inverse de assign
concatene <- do.call(rbind, lapply(paste("entree", anMin:anMax, sep = "_"), get))
# À FAIRE : problème pour les entrées où la date est manquante
# On va créer une base par année d'entrée
anMin <- donnees_tidyverse %>% pull(date_entree) %>% lubridate::year() %>% min(na.rm = TRUE)
anMax <- donnees_tidyverse %>% pull(date_entree) %>% lubridate::year() %>% max(na.rm = TRUE)
for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"),
         donnees_tidyverse %>% filter(lubridate::year(date_entree) == as.name(i)))
}
# On va désormais empiler toutes les bases (concaténation par colonne)
# purrr::reduce applique la fonction bind_rows à l'ensemble des bases issues du purrr::map
# get permet de faire le chemin inverse de assign
concatene <- purrr::map(paste("entree", anMin:anMax, sep = "_"), get) %>% 
  purrr::reduce(bind_rows)
# On va créer une base par année d'entrée
anMin <- min(lubridate::year(donnees_datatable$date_entree), na.rm = TRUE)
anMax <- max(lubridate::year(donnees_datatable$date_entree), na.rm = TRUE)
for (i in anMin:anMax) {
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(paste("entree", i, sep = "_"), donnees_datatable[lubridate::year(donnees_datatable$date_entree) == i, ])
}
# On va désormais empiler toutes les bases (concaténation par colonne)
# do.call applique la fonction rbind à l'ensemble des bases issues du lapply
# get permet de faire le chemin inverse de assign
concatene <- rbindlist(lapply(paste("entree", anMin:anMax, sep = "_"), get))

18.4 Boucles for (second exemple)

/* On recherche toutes les valeurs de CSP différentes et on les met dans une variable.
   On appelle la proc SQL :
   - utilisation du quit et non run à la fin
   - on récupère toutes les valeurs différentes de CSP, séparés par un espace (separated by)
   - s'il y a un espace dans les noms, on le remplace par _ 
   - on les met dans la macro-variable liste_csp
   - on trier la liste par valeur de CSP */
/* On crée une variable de CSP formaté sans les accents et les espaces */
data donnees_sas;
  set donnees_sas;
  /* SAS ne pourra pas créer des bases de données avec des noms accentués */
  /* On supprime dans le nom les lettres accentués. On le fait avec la fonction Translate */
  CSPF2 = tranwrd(strip(CSPF), " ", "_");
  CSPF2 = translate(CSPF2, "eeeeaacio", "éèêëàâçîô");
run;

/* Boucles et macros en SAS */
/* Les boucles ne peuvent être utilisées que dans le cadre de macros */
/* Ouverture de la macro */
%macro Boucles(base = donnees_sas, var = CSPF2);
  /* Les modalités de la variable */
  proc sql noprint;select distinct &var. into :liste separated by " " from &base. order by &var.;quit;
  /* On affiche la liste de ces modalités */
  %put &liste.;
  /* %let permet à SAS d'affecter une valeur à une variable en dehors d'une manipulation de base de données */
  /* %sysfunc indique à SAS qu'il doit utiliser la fonction countw dans le cadre d'une macro (pas important) */
  /* countw est une fonction qui compte le nombre de mots (séparés par un espace) d'une chaîne de caractères */
  /* => on compte le nombre de CSP différentes */
  %let nb = %sysfunc(countw(&liste.));
  %put Nombre de modalités différentes : &nb.;
  /* On itère pour chaque CSP différente ... */
  %do i = 1 %to &nb.;
    /* %scan : donne le i-ème mot de &liste. (les mots sont séparés par un espace) */
    /* => on récupère donc la CSP numéro i */
    %let j = %scan(&liste., &i.);
    %put Variable : &j.;
    /* On crée une base avec seulement les individus de la CSP correspondante */
    data &var.;set donnees_sas;if &var. = "&j.";run;
  %end;
/* Fermeture de la macro */
%mend Boucles;
/* Lancement de la macro */
%Boucles(base = donnees_sas, var = CSPF2);
# Base par CSP
for (i in unique(donnees_rbase$cspf)) {
  # Met en minuscule et enlève les accents
  nomBase <- tolower(chartr("éèêëàâçîô", "eeeeaacio", i))
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_rbase[which(donnees_rbase$cspf == i), ])
}
# Base par CSP
for (i in donnees_tidyverse %>% distinct(cspf) %>% pull()) {
  # Met en minuscule et enlève les accents
  nomBase <- chartr("éèêëàâçîô", "eeeeaacio", i) %>% tolower()
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_tidyverse %>% 
           filter(cspf == as.name(i)))
}
# Créer une base pour chaque individu d'une certaine CSP
for (i in unique(donnees_datatable$cspf)) {
  # Met en minuscule et enlève les accents
  nomBase <- tolower(chartr("éèêëàâçîô", "eeeeaacio", i))
  # assign permet de faire passer une chaîne de caractères en variable R
  assign(nomBase, donnees_datatable[donnees_datatable$cspf == i, ])
}

18.5 Autres transcriptions de fonctions SAS vers R

/* À FAIRE */
# Mesurer la durée d'exécution d'un programme
system.time(donnees_rbase <- donnees_rbase[order(donnees_rbase$identifiant, donnees_rbase$date_entree, na.last = FALSE), ])
# En SAS : include("chemin")
# En R : source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)
# Mesurer la durée d'exécution d'un programme
system.time(donnees_tidyverse <- donnees_tidyverse %>% 
              arrange(identifiant, date_entree))
# En SAS : include("chemin")
# En R : source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)
# Mesurer la durée d'exécution d'un programme
system.time(setorder(donnees_datatable, "identifiant", "date_entree", na.last = FALSE))
# En SAS : include("chemin")
# En R : source("chemin", encoding = "utf-8", echo = TRUE, max.deparse.length = 1e3)

19 Débogage

19.1 Outils d’aide au débogage

options symbolgen mprint mlogic;
%macro Debogage;
  %local phrase i j;
  %let phrase = Voici une phrase;
  %do i = 1 %to %sysfunc(countw(&phrase.));
    %let j = %scan(&phrase., &i.);
    %put Mot n°&i. = &j.;
  %end;
%mend Debogage;
%Debogage;
options nosymbolgen nomprint nomlogic;
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()
# À FAIRE : creuser
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()
#phrase <- c("voici", "une", "phrase")
#options(error=recover)
#for (i in phrase) print(k)
#options(error=NULL)

# À FAIRE : autres outils
#traceback()
#browser()

20 Points de vigilance en SAS

20.1 Emploi des guillemets et double guillemets

/* Quelques points de vigilance en SAS (à ne connaître que si on est amené à modifier le programme SAS, pas utiles sinon) */
/* Double guillemets pour les macro-variables */
%let a = Bonjour;
%put '&a.'; /* Incorrect */
%put "&a."; /* Correct */
# Sans objet en R
# Sans objet en R
# Sans objet en R

20.2 Macro-variable définie avec un statut global avant son appel dans le cadre d’un statut local

%macro test;
  %let reponse = oui;
%mend test;
%test;
/* 1. Erreur car &reponse. n'est défini que dans le cas d'un environnement local */ 
%put &reponse.;
/* 2. Défini auparavant dans un environnement global, elle change de valeur à l'appel de la fonction */
%let reponse = non;
%put Reponse : &reponse.;
%test;
%put Reponse après la macro : &reponse.;
/* 3. Problème corrigé, en imposant la variable à local dans la macro */
%macro test2;
  %local reponse;
  %let reponse = oui;
%mend test2;
%let reponse = non;
%put Reponse : &reponse.;
%test2;
%put Année après la macro : &reponse.;
# Sans objet en R
# Sans objet en R
# Sans objet en R

21 Fin du programme

21.1 Taille des objets en mémoire

/* Taille d'une base de données */
proc sql;
  select libname, memname, filesize format = sizekmg., filesize format = sizek.
  from Dictionary.Tables
  where libname = "WORK" and memname = upcase("donnees_sas") and memtype = "DATA";
quit;
# Taille, en mémoire, d'une base (en Mb)
format(object.size(donnees_rbase), nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort(sapply(ls(), function(x){object.size(get(x))}), decreasing = TRUE)/10**9
# Taille, en mémoire, d'une base (en Mb)
donnees_tidyverse %>% 
  object.size() %>% 
  format(nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort(sapply(ls(), function(x){object.size(get(x))}), decreasing = TRUE)/10**9
# Liste des bases de données en mémoire
data.table::tables() 

# Taille, en mémoire, d'une base (en Mb)
format(object.size(donnees_datatable), nsmall = 3, digits = 2, unit = "Mb")

# Taille des objets en mémoire, en Gb
sort(sapply(ls(), function(x){object.size(get(x))}), decreasing = TRUE)/10**9

21.2 Supprimer des bases

/* Supprimer une base */
proc datasets lib = work nolist;delete donnees_sas;run;

/* Supprimer toutes les bases dans la work */
proc datasets lib = work nolist kill;run;
# Supprimer une base
#rm(donnees_rbase)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())
# Supprimer une base
#rm(donnees_tidyverse)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())
# Supprimer une base
#rm(donnees_datatable)

# Supprimer toutes les bases et tous les objets de la mémoire vive
#rm(list = ls())